Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions hosts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ import cursor from './cursor';
import openclaw from './openclaw';
import hermes from './hermes';
import gbrain from './gbrain';
import zed from './zed';

/** All registered host configs. Add new hosts here. */
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain];
export const ALL_HOST_CONFIGS: HostConfig[] = [claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, zed];

/** Map from host name to config. */
export const HOST_CONFIG_MAP: Record<string, HostConfig> = Object.fromEntries(
Expand Down Expand Up @@ -65,4 +66,4 @@ export function getExternalHosts(): HostConfig[] {
}

// Re-export individual configs for direct import
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain };
export { claude, codex, factory, kiro, opencode, slate, cursor, openclaw, hermes, gbrain, zed };
51 changes: 51 additions & 0 deletions hosts/zed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import type { HostConfig } from '../scripts/host-config';

const zed: HostConfig = {
name: 'zed',
displayName: 'Zed',
cliCommand: 'zed',
cliAliases: [],

globalRoot: '.agents/skills/gstack',
localSkillRoot: '.agents/skills/gstack',
hostSubdir: '.zed',
usesEnvVars: true,

frontmatter: {
mode: 'allowlist',
keepFields: ['name', 'description'],
descriptionLimit: 1024,
descriptionLimitBehavior: 'truncate',
},

generation: {
generateMetadata: false,
skipSkills: ['codex'],
},

maxFileBytes: 100 * 1024,

pathRewrites: [
{ from: '~/.claude/skills/gstack', to: '~/.agents/skills/gstack' },
{ from: '.claude/skills/gstack', to: '.agents/skills/gstack' },
{ from: '.claude/skills', to: '.agents/skills' },
],

suppressedResolvers: ['GBRAIN_CONTEXT_LOAD', 'GBRAIN_SAVE_RESULTS'],

runtimeRoot: {
globalSymlinks: ['bin', 'browse/dist', 'browse/bin', 'design/dist', 'gstack-upgrade', 'ETHOS.md'],
globalFiles: {
'review': ['checklist.md', 'TODOS-format.md'],
},
},

install: {
prefixable: false,
linkingStrategy: 'symlink-generated',
},

learningsMode: 'basic',
};

export default zed;
47 changes: 47 additions & 0 deletions scripts/gen-skill-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,45 @@ function extractHookSafetyProse(tmplContent: string): string | null {

const GENERATED_HEADER = `<!-- AUTO-GENERATED from {{SOURCE}} — do not edit directly -->\n<!-- Regenerate: bun run gen:skill-docs -->\n`;

/**
* Truncate a SKILL.md to at most `maxBytes` bytes, preserving the frontmatter
* and trimming the body at the last complete line that fits, then appending a
* one-line notice. Called after the generated header has been inserted so the
* header bytes are already included in the size budget.
*/
function truncateToMaxBytes(content: string, maxBytes: number): string {
if (Buffer.byteLength(content, 'utf-8') <= maxBytes) return content;

const notice = '\n\n> [Truncated to fit host file size limit. Full skill: ~/.claude/skills/gstack]\n';
const noticeBytes = Buffer.byteLength(notice, 'utf-8');
const budget = maxBytes - noticeBytes;

// Split at the end of the closing frontmatter `---` line.
const fmEnd = content.indexOf('\n---\n', 3);
const splitAt = fmEnd !== -1 ? fmEnd + 5 : 0;
const frontmatter = content.slice(0, splitAt);
const body = content.slice(splitAt);

const fmBytes = Buffer.byteLength(frontmatter, 'utf-8');
const bodyBudget = Math.max(0, budget - fmBytes);

let bodyTrunc = body;
if (Buffer.byteLength(body, 'utf-8') > bodyBudget) {
const lines = body.split('\n');
const kept: string[] = [];
let used = 0;
for (const line of lines) {
const lb = Buffer.byteLength(line + '\n', 'utf-8');
if (used + lb > bodyBudget) break;
kept.push(line);
used += lb;
}
bodyTrunc = kept.join('\n');
}

return frontmatter + bodyTrunc + notice;
}

/**
* Process external host output: routing, frontmatter, path rewrites, metadata.
* Shared between Codex and Factory (and future external hosts).
Expand Down Expand Up @@ -683,6 +722,14 @@ function processTemplate(tmplPath: string, host: Host = 'claude'): { outputPath:
content = header + content;
}

// Config-driven: truncate to maxFileBytes after header insertion (e.g. Zed's 100KB limit).
if (host !== 'claude' && !symlinkLoop) {
const hostCfg = getHostConfig(host);
if (hostCfg.maxFileBytes && Buffer.byteLength(content, 'utf-8') > hostCfg.maxFileBytes) {
content = truncateToMaxBytes(content, hostCfg.maxFileBytes);
}
}

// Catalog trim (Claude only — external hosts have their own frontmatter shapes)
let catalogParts: CatalogParts | null = null;
if (host === 'claude' && CATALOG_MODE === 'trim') {
Expand Down
7 changes: 7 additions & 0 deletions scripts/host-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ export interface HostConfig {
// --- Content Rewrites ---
/** Literal string replacements on generated SKILL.md content. Order matters, replaceAll. */
pathRewrites: Array<{ from: string; to: string }>;
/**
* Hard byte ceiling for generated SKILL.md files (e.g. 100 * 1024 for Zed).
* When set, the body is truncated at the last complete line that keeps the
* total file under this limit and a one-line truncation notice is appended.
* The frontmatter is never truncated.
*/
maxFileBytes?: number;
/** Tool name string replacements on content. */
toolRewrites?: Record<string, string>;
/** Resolver functions that return empty string for this host. */
Expand Down
63 changes: 59 additions & 4 deletions setup
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ TEAM_MODE=0
NO_TEAM_MODE=0
while [ $# -gt 0 ]; do
case "$1" in
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host) [ -z "$2" ] && echo "Missing value for --host (expected claude, codex, kiro, factory, opencode, zed, openclaw, hermes, gbrain, or auto)" >&2 && exit 1; HOST="$2"; shift 2 ;;
--host=*) HOST="${1#--host=}"; shift ;;
--local) LOCAL_INSTALL=1; shift ;;
--prefix) SKILL_PREFIX=1; SKILL_PREFIX_FLAG=1; shift ;;
Expand All @@ -97,7 +97,7 @@ while [ $# -gt 0 ]; do
done

case "$HOST" in
claude|codex|kiro|factory|opencode|auto) ;;
claude|codex|kiro|factory|opencode|zed|auto) ;;
openclaw)
echo ""
echo "OpenClaw integration uses a different model — OpenClaw spawns Claude Code"
Expand Down Expand Up @@ -132,7 +132,7 @@ case "$HOST" in
echo "GBrain setup and brain skills ship from the GBrain repo."
echo ""
exit 0 ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
*) echo "Unknown --host value: $HOST (expected claude, codex, kiro, factory, opencode, zed, openclaw, hermes, gbrain, or auto)" >&2; exit 1 ;;
esac

# ─── Resolve skill prefix preference ─────────────────────────
Expand Down Expand Up @@ -196,14 +196,16 @@ INSTALL_CODEX=0
INSTALL_KIRO=0
INSTALL_FACTORY=0
INSTALL_OPENCODE=0
INSTALL_ZED=0
if [ "$HOST" = "auto" ]; then
command -v claude >/dev/null 2>&1 && INSTALL_CLAUDE=1
command -v codex >/dev/null 2>&1 && INSTALL_CODEX=1
command -v kiro-cli >/dev/null 2>&1 && INSTALL_KIRO=1
command -v droid >/dev/null 2>&1 && INSTALL_FACTORY=1
command -v opencode >/dev/null 2>&1 && INSTALL_OPENCODE=1
command -v zed >/dev/null 2>&1 && INSTALL_ZED=1
# If none found, default to claude
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ]; then
if [ "$INSTALL_CLAUDE" -eq 0 ] && [ "$INSTALL_CODEX" -eq 0 ] && [ "$INSTALL_KIRO" -eq 0 ] && [ "$INSTALL_FACTORY" -eq 0 ] && [ "$INSTALL_OPENCODE" -eq 0 ] && [ "$INSTALL_ZED" -eq 0 ]; then
INSTALL_CLAUDE=1
fi
elif [ "$HOST" = "claude" ]; then
Expand All @@ -216,6 +218,8 @@ elif [ "$HOST" = "factory" ]; then
INSTALL_FACTORY=1
elif [ "$HOST" = "opencode" ]; then
INSTALL_OPENCODE=1
elif [ "$HOST" = "zed" ]; then
INSTALL_ZED=1
fi

migrate_direct_codex_install() {
Expand Down Expand Up @@ -1076,6 +1080,57 @@ if [ "$INSTALL_OPENCODE" -eq 1 ]; then
echo " opencode skills: $OPENCODE_SKILLS"
fi

# 6d. Install for Zed
if [ "$INSTALL_ZED" -eq 1 ]; then
ZED_SKILLS="$HOME/.agents/skills"
ZED_GSTACK="$ZED_SKILLS/gstack"
ZED_GEN_DIR="$SOURCE_GSTACK_DIR/.zed/skills"
mkdir -p "$ZED_SKILLS"

# Generate Zed-specific skill docs into .zed/skills/
( cd "$SOURCE_GSTACK_DIR" && PATH="$HOME/.bun/bin:$PATH" bun run gen:skill-docs --host zed ) || \
echo " warning: gen:skill-docs --host zed failed — skills may be stale" >&2

# Runtime asset root
[ -L "$ZED_GSTACK" ] && rm -f "$ZED_GSTACK"
mkdir -p "$ZED_GSTACK" "$ZED_GSTACK/browse" "$ZED_GSTACK/gstack-upgrade" "$ZED_GSTACK/review"
_link_or_copy "$SOURCE_GSTACK_DIR/bin" "$ZED_GSTACK/bin"
_link_or_copy "$SOURCE_GSTACK_DIR/browse/dist" "$ZED_GSTACK/browse/dist"
_link_or_copy "$SOURCE_GSTACK_DIR/browse/bin" "$ZED_GSTACK/browse/bin"
if [ -f "$SOURCE_GSTACK_DIR/ETHOS.md" ]; then
_link_or_copy "$SOURCE_GSTACK_DIR/ETHOS.md" "$ZED_GSTACK/ETHOS.md"
fi
if [ -f "$ZED_GEN_DIR/gstack-upgrade/SKILL.md" ]; then
_link_or_copy "$ZED_GEN_DIR/gstack-upgrade/SKILL.md" "$ZED_GSTACK/gstack-upgrade/SKILL.md"
fi
for f in checklist.md TODOS-format.md; do
if [ -f "$SOURCE_GSTACK_DIR/review/$f" ]; then
_link_or_copy "$SOURCE_GSTACK_DIR/review/$f" "$ZED_GSTACK/review/$f"
fi
done

# Root SKILL.md with path rewrites for Zed
sed -e "s|~/.claude/skills/gstack|~/.agents/skills/gstack|g" \
-e "s|\.claude/skills/gstack|.agents/skills/gstack|g" \
-e "s|\.claude/skills|.agents/skills|g" \
"$SOURCE_GSTACK_DIR/SKILL.md" > "$ZED_GSTACK/SKILL.md"

if [ ! -d "$ZED_GEN_DIR" ]; then
echo " warning: .zed/skills/ not found — run 'bun run gen:skill-docs --host zed' manually" >&2
else
for skill_dir in "$ZED_GEN_DIR"/gstack*/; do
[ -f "$skill_dir/SKILL.md" ] || continue
skill_name="$(basename "$skill_dir")"
target_dir="$ZED_SKILLS/$skill_name"
mkdir -p "$target_dir"
cp "$skill_dir/SKILL.md" "$target_dir/SKILL.md"
done
echo "gstack ready (zed)."
echo " browse: $BROWSE_BIN"
echo " zed skills: $ZED_SKILLS"
fi
fi

# 7. Create .agents/ sidecar symlinks for the real Codex skill target.
# The root Codex skill ends up pointing at $SOURCE_GSTACK_DIR/.agents/skills/gstack,
# so the runtime assets must live there for both global and repo-local installs.
Expand Down