jeo-code 0.1.0 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/README.ja.md +160 -0
  2. package/README.ko.md +160 -0
  3. package/README.md +115 -297
  4. package/README.zh.md +160 -0
  5. package/package.json +11 -6
  6. package/scripts/install.sh +28 -28
  7. package/scripts/uninstall.sh +17 -15
  8. package/src/AGENTS.md +50 -0
  9. package/src/agent/AGENTS.md +49 -0
  10. package/src/agent/bash-fixups.ts +103 -0
  11. package/src/agent/compaction.ts +410 -19
  12. package/src/agent/config-schema.ts +119 -5
  13. package/src/agent/context-files.ts +314 -17
  14. package/src/agent/dev/AGENTS.md +36 -0
  15. package/src/agent/dev/advanced-analyzer.ts +12 -0
  16. package/src/agent/dev/evolution-bridge.ts +82 -0
  17. package/src/agent/dev/evolution-logger.ts +41 -0
  18. package/src/agent/dev/self-analysis.ts +64 -0
  19. package/src/agent/dev/self-improve.ts +24 -0
  20. package/src/agent/dev/spec-automation.ts +49 -0
  21. package/src/agent/engine.ts +808 -54
  22. package/src/agent/hooks.ts +273 -0
  23. package/src/agent/loop.ts +21 -1
  24. package/src/agent/memory.ts +201 -0
  25. package/src/agent/model-recency.ts +32 -0
  26. package/src/agent/output-minimizer.ts +108 -0
  27. package/src/agent/output-util.ts +64 -0
  28. package/src/agent/plan.ts +187 -0
  29. package/src/agent/seed.ts +52 -0
  30. package/src/agent/session.ts +235 -21
  31. package/src/agent/state.ts +286 -39
  32. package/src/agent/step-budget.ts +232 -0
  33. package/src/agent/subagents.ts +223 -26
  34. package/src/agent/task-tool.ts +272 -0
  35. package/src/agent/todo-tool.ts +87 -0
  36. package/src/agent/tokenizer.ts +117 -0
  37. package/src/agent/tool-registry.ts +54 -0
  38. package/src/agent/tools.ts +624 -103
  39. package/src/agent/web-search.ts +538 -0
  40. package/src/ai/AGENTS.md +44 -0
  41. package/src/ai/index.ts +1 -0
  42. package/src/ai/model-catalog-compat.ts +3 -1
  43. package/src/ai/model-catalog.ts +74 -9
  44. package/src/ai/model-discovery.ts +215 -17
  45. package/src/ai/model-manager.ts +346 -32
  46. package/src/ai/model-picker.ts +1 -1
  47. package/src/ai/model-registry.ts +4 -2
  48. package/src/ai/pricing.ts +84 -0
  49. package/src/ai/provider-registry.ts +23 -0
  50. package/src/ai/provider-status.ts +60 -16
  51. package/src/ai/providers/AGENTS.md +42 -0
  52. package/src/ai/providers/anthropic.ts +250 -31
  53. package/src/ai/providers/antigravity.ts +219 -0
  54. package/src/ai/providers/errors.ts +15 -1
  55. package/src/ai/providers/gemini.ts +196 -13
  56. package/src/ai/providers/ollama.ts +37 -7
  57. package/src/ai/providers/openai-responses.ts +173 -0
  58. package/src/ai/providers/openai.ts +64 -12
  59. package/src/ai/sse.ts +4 -1
  60. package/src/ai/types.ts +18 -1
  61. package/src/auth/AGENTS.md +41 -0
  62. package/src/auth/callback-server.ts +6 -1
  63. package/src/auth/flows/AGENTS.md +32 -0
  64. package/src/auth/flows/antigravity.ts +151 -0
  65. package/src/auth/flows/google-project.ts +190 -0
  66. package/src/auth/flows/google.ts +39 -18
  67. package/src/auth/flows/index.ts +15 -5
  68. package/src/auth/flows/openai.ts +2 -2
  69. package/src/auth/oauth.ts +8 -0
  70. package/src/auth/refresh.ts +44 -27
  71. package/src/auth/storage.ts +149 -26
  72. package/src/auth/types.ts +1 -1
  73. package/src/autopilot.ts +362 -0
  74. package/src/bun-imports.d.ts +4 -0
  75. package/src/cli/AGENTS.md +39 -0
  76. package/src/cli/runner.ts +148 -14
  77. package/src/cli.ts +13 -4
  78. package/src/commands/AGENTS.md +40 -0
  79. package/src/commands/approve.ts +62 -3
  80. package/src/commands/auth.ts +167 -25
  81. package/src/commands/chat.ts +37 -8
  82. package/src/commands/deep-interview.ts +633 -175
  83. package/src/commands/doctor.ts +84 -37
  84. package/src/commands/evolve-core.ts +18 -0
  85. package/src/commands/evolve.ts +2 -1
  86. package/src/commands/export.ts +176 -0
  87. package/src/commands/gjc.ts +52 -0
  88. package/src/commands/launch.ts +3549 -240
  89. package/src/commands/mcp.ts +3 -3
  90. package/src/commands/ooo-seed.ts +19 -0
  91. package/src/commands/ralplan.ts +253 -35
  92. package/src/commands/resume.ts +1 -1
  93. package/src/commands/session.ts +183 -0
  94. package/src/commands/setup-helpers.ts +10 -3
  95. package/src/commands/setup.ts +57 -16
  96. package/src/commands/skills.ts +78 -18
  97. package/src/commands/state.ts +198 -0
  98. package/src/commands/status.ts +84 -0
  99. package/src/commands/team.ts +340 -212
  100. package/src/commands/ultragoal.ts +122 -61
  101. package/src/commands/update.ts +244 -0
  102. package/src/ledger.ts +270 -0
  103. package/src/mcp/AGENTS.md +38 -0
  104. package/src/mcp/server.ts +115 -14
  105. package/src/mcp/tools.ts +42 -22
  106. package/src/md-modules.d.ts +4 -0
  107. package/src/prompts/AGENTS.md +41 -0
  108. package/src/prompts/agents/AGENTS.md +35 -0
  109. package/src/prompts/agents/architect.md +35 -0
  110. package/src/prompts/agents/critic.md +37 -0
  111. package/src/prompts/agents/executor.md +36 -0
  112. package/src/prompts/agents/planner.md +37 -0
  113. package/src/prompts/skills/AGENTS.md +36 -0
  114. package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
  115. package/src/prompts/skills/deep-dive/SKILL.md +13 -0
  116. package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
  117. package/src/prompts/skills/deep-interview/SKILL.md +12 -0
  118. package/src/prompts/skills/gjc/AGENTS.md +31 -0
  119. package/src/prompts/skills/gjc/SKILL.md +15 -0
  120. package/src/prompts/skills/ralplan/AGENTS.md +31 -0
  121. package/src/prompts/skills/ralplan/SKILL.md +11 -0
  122. package/src/prompts/skills/team/AGENTS.md +31 -0
  123. package/src/prompts/skills/team/SKILL.md +11 -0
  124. package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
  125. package/src/prompts/skills/ultragoal/SKILL.md +11 -0
  126. package/src/skills/AGENTS.md +38 -0
  127. package/src/skills/catalog.ts +565 -31
  128. package/src/tui/AGENTS.md +43 -0
  129. package/src/tui/app.ts +1181 -92
  130. package/src/tui/components/AGENTS.md +42 -0
  131. package/src/tui/components/ascii-art.ts +257 -15
  132. package/src/tui/components/autocomplete.ts +98 -16
  133. package/src/tui/components/autopilot-status.ts +65 -0
  134. package/src/tui/components/category-index.ts +49 -0
  135. package/src/tui/components/code-view.ts +54 -11
  136. package/src/tui/components/color.ts +171 -2
  137. package/src/tui/components/config-panel.ts +82 -15
  138. package/src/tui/components/duration.ts +38 -0
  139. package/src/tui/components/evolution.ts +3 -3
  140. package/src/tui/components/footer.ts +91 -42
  141. package/src/tui/components/forge.ts +426 -31
  142. package/src/tui/components/hints.ts +54 -0
  143. package/src/tui/components/hud.ts +73 -0
  144. package/src/tui/components/index.ts +4 -0
  145. package/src/tui/components/input-box.ts +150 -0
  146. package/src/tui/components/layout.ts +11 -3
  147. package/src/tui/components/live-model-picker.ts +108 -0
  148. package/src/tui/components/markdown-table.ts +140 -0
  149. package/src/tui/components/markdown-text.ts +97 -0
  150. package/src/tui/components/meter.ts +4 -1
  151. package/src/tui/components/model-picker.ts +3 -2
  152. package/src/tui/components/provider-picker.ts +3 -2
  153. package/src/tui/components/section.ts +70 -0
  154. package/src/tui/components/select-list.ts +40 -10
  155. package/src/tui/components/skill-picker.ts +25 -0
  156. package/src/tui/components/slash.ts +244 -21
  157. package/src/tui/components/status.ts +272 -11
  158. package/src/tui/components/step-timeline.ts +218 -0
  159. package/src/tui/components/stream.ts +26 -9
  160. package/src/tui/components/themes.ts +212 -6
  161. package/src/tui/components/todo-card.ts +47 -0
  162. package/src/tui/components/tool-list.ts +58 -12
  163. package/src/tui/components/transcript.ts +120 -0
  164. package/src/tui/components/update-box.ts +31 -0
  165. package/src/tui/components/welcome.ts +162 -0
  166. package/src/tui/components/width.ts +163 -0
  167. package/src/tui/monitoring/AGENTS.md +31 -0
  168. package/src/tui/monitoring/hud-view.ts +55 -0
  169. package/src/tui/renderer.ts +112 -3
  170. package/src/tui/terminal.ts +40 -33
  171. package/src/util/AGENTS.md +39 -0
  172. package/src/util/clipboard-image.ts +118 -0
  173. package/src/util/env.ts +12 -0
  174. package/src/util/provider-error.ts +78 -0
  175. package/src/util/retry.ts +91 -6
  176. package/src/util/update-check.ts +64 -0
  177. package/src/commands/models.ts +0 -104
@@ -1,5 +1,7 @@
1
+ import { qualifyModelId } from "../ai/model-manager";
2
+ import type { ProviderName } from "../ai/types";
1
3
  import { createInterface } from "node:readline/promises";
2
- import { saveGlobalConfig, readGlobalConfig, type Config } from "../agent/state";
4
+ import { saveGlobalConfig, readGlobalConfig, readRawGlobalConfig, type Config } from "../agent/state";
3
5
  import {
4
6
  interactiveLogin,
5
7
  getStoredOAuth,
@@ -24,7 +26,7 @@ function reportModelChoice(r: { warning?: string; suggestions: string[] }): void
24
26
  type ProviderChoice = "anthropic" | "openai" | "gemini" | "ollama" | "lmstudio" | "openai-compatible";
25
27
 
26
28
  const DEFAULT_MODELS: Record<ProviderChoice, string> = {
27
- anthropic: "claude-3-5-sonnet-20241022",
29
+ anthropic: "claude-sonnet-4-5",
28
30
  openai: "gpt-4o",
29
31
  gemini: "gemini-2.0-flash",
30
32
  ollama: "ollama/llama3.1:8b",
@@ -64,10 +66,20 @@ async function listOpenAiCompatibleModels(baseUrl: string, apiKey?: string): Pro
64
66
  }
65
67
 
66
68
  export async function runSetupCommand(): Promise<void> {
69
+ // `setup` is interactive; on a non-TTY (piped/CI) stdin readline can't prompt and
70
+ // would reject with a cryptic "readline was closed". Fail with clear guidance instead.
71
+ if (!process.stdin.isTTY) {
72
+ console.log(
73
+ "jeo setup needs an interactive terminal (TTY).\n" +
74
+ "Non-interactive options: set env vars (ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY, JEO_DEFAULT_MODEL, OLLAMA_HOST),\n" +
75
+ "run 'jeo auth login <anthropic|openai|gemini|antigravity>' for OAuth, or edit ~/.jeo/config.json directly. Verify with 'jeo doctor'.",
76
+ );
77
+ return;
78
+ }
67
79
  const current = await readGlobalConfig();
68
80
  const rl = createInterface({ input: process.stdin, output: process.stdout });
69
81
 
70
- console.log("\n=== @jeo-code CLI Configuration (joc setup) ===");
82
+ console.log("\n=== @jeo-code CLI Configuration (jeo setup) ===");
71
83
  console.log("Configure providers, API keys / OAuth tokens, and default model.\n");
72
84
 
73
85
  console.log("Available provider types:");
@@ -87,7 +99,11 @@ export async function runSetupCommand(): Promise<void> {
87
99
  };
88
100
  const choice = map[sel] ?? "skip";
89
101
 
90
- const next: Config = JSON.parse(JSON.stringify(current)) as Config;
102
+ // Build the persisted config from the RAW on-disk state (NOT the env-overlaid
103
+ // `current`), so `jeo setup` never bakes env-only values — OAuth bearer tokens
104
+ // (*_OAUTH_TOKEN), JEO_DEFAULT_MODEL, JEO_*_MODEL roles, OLLAMA_HOST/OPENAI_BASE_URL —
105
+ // permanently into ~/.jeo/config.json. `current` is still used for display defaults.
106
+ const next: Config = JSON.parse(JSON.stringify(await readRawGlobalConfig())) as Config;
91
107
  next.providers = next.providers || {};
92
108
  next.oauth = next.oauth || {};
93
109
 
@@ -104,7 +120,12 @@ export async function runSetupCommand(): Promise<void> {
104
120
  } else {
105
121
  const flow = OAUTH_FLOW_REGISTRY[choice as AuthProvider];
106
122
  if (!flow.verifiedEndToEnd && flow.note) console.log(`Note: ${flow.note}`);
123
+ // Abort the pending "Paste redirect URL…" question once the flow settles —
124
+ // otherwise it survives the SUCCESS/FAILED result, reprints its prompt, and
125
+ // QUEUES IN FRONT of the API-key fallback question below (setup looked hung).
126
+ const ac = new AbortController();
107
127
  const ctrl: OAuthController = {
128
+ signal: ac.signal,
108
129
  onAuth: ({ url, instructions }) => {
109
130
  console.log(`Opening browser:\n ${url}\n`);
110
131
  if (instructions) console.log(instructions + "\n");
@@ -112,10 +133,17 @@ export async function runSetupCommand(): Promise<void> {
112
133
  },
113
134
  onProgress: msg => console.log(` … ${msg}`),
114
135
  onManualCodeInput: async () =>
115
- (await rl.question("Paste redirect URL or code (or wait for the browser callback): ")).trim(),
136
+ (await rl.question("Paste redirect URL or code (or wait for the browser callback): ", { signal: ac.signal })).trim(),
116
137
  };
117
138
  try {
118
- const { email } = await interactiveLogin(choice as AuthProvider, ctrl);
139
+ let email: string | undefined;
140
+ try {
141
+ ({ email } = await interactiveLogin(choice as AuthProvider, ctrl));
142
+ } finally {
143
+ // Must fire BEFORE the catch's API-key question below, or that
144
+ // question queues behind the stale paste prompt.
145
+ ac.abort();
146
+ }
119
147
  const stored = await getStoredOAuth(choice as AuthProvider);
120
148
  if (stored) next.oauth[choice] = stored;
121
149
  console.log(`[SUCCESS] OAuth login complete for ${choice}${email ? ` (${email})` : ""}.`);
@@ -126,12 +154,17 @@ export async function runSetupCommand(): Promise<void> {
126
154
  if (key.trim()) next.providers[choice] = key.trim();
127
155
  }
128
156
  }
129
- console.log(`\nRecommended ${choice} models:`);
130
- for (const m of recommendedModelsFor(choice)) console.log(` - ${m}`);
131
- const dm = await rl.question(`Default model for ${choice} [${recommendedModelsFor(choice)[0]?.split(" ")[0] ?? DEFAULT_MODELS[choice]}]: `);
132
- const picked = chooseDefaultModel(dm, choice);
157
+ const openAiCodexOnly = choice === "openai" && !!next.oauth.openai && !next.providers.openai;
158
+ const recommended = recommendedModelsFor(choice, 5, { codex: openAiCodexOnly });
159
+ const fallbackModel = recommended[0]?.split(" ")[0] ?? DEFAULT_MODELS[choice];
160
+ console.log(`\nRecommended ${choice}${openAiCodexOnly ? " Codex OAuth" : ""} models:`);
161
+ for (const m of recommended) console.log(` - ${m}`);
162
+ const dm = await rl.question(`Default model for ${choice} [${fallbackModel}]: `);
163
+ const rawInput = dm.trim();
164
+ const qualified = rawInput ? qualifyModelId(rawInput, choice as ProviderName) : fallbackModel;
165
+ const picked = chooseDefaultModel(qualified, choice as ProviderName);
133
166
  reportModelChoice(picked);
134
- next.defaultModel = picked.model || DEFAULT_MODELS[choice];
167
+ next.defaultModel = picked.model || fallbackModel;
135
168
  } else if (choice === "ollama") {
136
169
  const url = await rl.question(`Ollama base URL [${current.ollamaBaseUrl || DEFAULT_BASE_URLS.ollama}]: `);
137
170
  next.ollamaBaseUrl = normalizeBaseUrl(url, current.ollamaBaseUrl || DEFAULT_BASE_URLS.ollama!);
@@ -141,11 +174,15 @@ export async function runSetupCommand(): Promise<void> {
141
174
  console.log("Detected local Ollama models:");
142
175
  models.slice(0, 20).forEach((m, i) => console.log(` - ${m}`));
143
176
  const def = await rl.question(`Default model (ollama/<name>) [${"ollama/" + (models[0] ?? "llama3.1:8b")}]: `);
144
- next.defaultModel = def.trim() || `ollama/${models[0] ?? "llama3.1:8b"}`;
177
+ const rawInput = def.trim();
178
+ const qualified = rawInput ? qualifyModelId(rawInput, "ollama") : `ollama/${models[0] ?? "llama3.1:8b"}`;
179
+ next.defaultModel = qualified;
145
180
  reportModelChoice(chooseDefaultModel(next.defaultModel, "ollama"));
146
181
  } else {
147
182
  console.log(" (no models detected — Ollama not reachable, defaulting to llama3.1:8b)");
148
- const picked = chooseDefaultModel(await rl.question(`Default model [${DEFAULT_MODELS.ollama}]: `), "ollama");
183
+ const rawInput = (await rl.question(`Default model [${DEFAULT_MODELS.ollama}]: `)).trim();
184
+ const qualified = rawInput ? qualifyModelId(rawInput, "ollama") : DEFAULT_MODELS.ollama;
185
+ const picked = chooseDefaultModel(qualified, "ollama");
149
186
  reportModelChoice(picked);
150
187
  next.defaultModel = picked.model || DEFAULT_MODELS.ollama;
151
188
  }
@@ -163,11 +200,15 @@ export async function runSetupCommand(): Promise<void> {
163
200
  console.log("Detected models:");
164
201
  models.slice(0, 20).forEach(m => console.log(` - ${m}`));
165
202
  const def = await rl.question(`Default model (openai/<name>) [openai/${models[0]}]: `);
166
- next.defaultModel = def.trim() || `openai/${models[0]}`;
203
+ const rawInput = def.trim();
204
+ const qualified = rawInput ? qualifyModelId(rawInput, "openai") : `openai/${models[0]}`;
205
+ next.defaultModel = qualified;
167
206
  reportModelChoice(chooseDefaultModel(next.defaultModel, "openai"));
168
207
  } else {
169
208
  console.log(" (no models detected — endpoint not reachable yet)");
170
- const picked = chooseDefaultModel(await rl.question(`Default model [${DEFAULT_MODELS[choice]}]: `), "openai");
209
+ const rawInput = (await rl.question(`Default model [${DEFAULT_MODELS[choice]}]: `)).trim();
210
+ const qualified = rawInput ? qualifyModelId(rawInput, "openai") : DEFAULT_MODELS[choice];
211
+ const picked = chooseDefaultModel(qualified, "openai");
171
212
  reportModelChoice(picked);
172
213
  next.defaultModel = picked.model || DEFAULT_MODELS[choice];
173
214
  }
@@ -184,7 +225,7 @@ export async function runSetupCommand(): Promise<void> {
184
225
  if (next.oauth && !next.oauth.anthropic && !next.oauth.openai && !next.oauth.gemini) delete next.oauth;
185
226
 
186
227
  await saveGlobalConfig(next);
187
- console.log("\n[SUCCESS] Configuration saved to ~/.joc/config.json");
228
+ console.log("\n[SUCCESS] Configuration saved to ~/.jeo/config.json");
188
229
  for (const line of buildSetupSummary(next)) console.log(line);
189
230
  console.log("");
190
231
  }
@@ -1,38 +1,98 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
- import { SKILLS, getSkill, formatSkill, skillNames } from "../skills/catalog";
4
- import { getLocalJocDir } from "../agent/state";
3
+ import { SKILLS, getSkillFrom, formatSkill, loadSkills, skillDirs } from "../skills/catalog";
4
+ import { getLocalJeoDir } from "../agent/state";
5
+
6
+ function editDistance(a: string, b: string): number {
7
+ const m = a.length, n = b.length;
8
+ if (m === 0) return n;
9
+ if (n === 0) return m;
10
+ const d = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
11
+ for (let i = 0; i <= m; i++) d[i][0] = i;
12
+ for (let j = 0; j <= n; j++) d[0][j] = j;
13
+ for (let i = 1; i <= m; i++) {
14
+ for (let j = 1; j <= n; j++) {
15
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
16
+ d[i][j] = Math.min(
17
+ d[i - 1][j] + 1, // deletion
18
+ d[i][j - 1] + 1, // insertion
19
+ d[i - 1][j - 1] + cost // substitution
20
+ );
21
+ }
22
+ }
23
+ return d[m][n];
24
+ }
25
+
26
+ function suggestSkills(name: string, known: string[]): string[] {
27
+ const q = name.toLowerCase();
28
+ if (!q) return [];
29
+ return known.filter(n => n.toLowerCase().startsWith(q) || editDistance(n.toLowerCase(), q) <= 2);
30
+ }
5
31
 
6
32
  export async function runSkillsCommand(args: string[] = []): Promise<void> {
7
- // `joc skills --write [dir]` materializes bundled skill docs to disk (gjc-style SKILL.md files).
8
- if (args[0] === "--write") {
9
- const cwd = process.cwd();
10
- const dir = args[1] ? path.resolve(cwd, args[1]) : path.join(getLocalJocDir(cwd), "skills");
33
+ const cwd = process.cwd();
34
+ const isJson = args.includes("--json");
35
+ const cleanArgs = args.filter(a => a !== "--json");
36
+
37
+ // `jeo skills --write [dir]` materializes bundled skill docs to disk (gjc-style SKILL.md files).
38
+ if (cleanArgs[0] === "--write") {
39
+ const dir = cleanArgs[1] ? path.resolve(cwd, cleanArgs[1]) : path.join(getLocalJeoDir(cwd), "skills");
11
40
  await fs.mkdir(dir, { recursive: true });
12
41
  for (const s of SKILLS) {
13
42
  const file = path.join(dir, `${s.name}.md`);
14
- await fs.writeFile(file, `# ${s.name}\n\n${formatSkill(s)}\n`, "utf-8");
43
+ await fs.writeFile(file, s.raw || `# ${s.name}\n\n${formatSkill(s)}\n`, "utf-8");
15
44
  }
16
45
  console.log(`Wrote ${SKILLS.length} skill docs to ${dir}`);
17
46
  return;
18
47
  }
19
48
 
20
- const name = args[0];
21
- if (name) {
22
- const skill = getSkill(name);
23
- if (!skill) {
24
- console.log(`Unknown skill: ${name}\nAvailable: ${skillNames().join(", ")}`);
49
+ // List/lookup over the MERGED set (bundled + ~/.jeo/skills + ~/.agents/skills + project dirs), matching the REPL /skill.
50
+ const skills = await loadSkills(cwd);
51
+ const command = cleanArgs[0];
52
+
53
+ if (!command || command === "list") {
54
+ if (isJson) {
55
+ console.log(JSON.stringify(skills.map(s => ({ name: s.name, summary: s.summary })), null, 2));
56
+ } else {
57
+ console.log("\n=== jeo skills ===");
58
+ console.log("Workflow skills (bundled + ~/.jeo/skills, ~/.agents/skills, project dirs) — 'jeo skills <name>' for details, --write to export:\n");
59
+ for (const s of skills) {
60
+ console.log(` ${s.name.padEnd(16)} ${s.summary}`);
61
+ }
62
+ console.log("\nInvoke: /skill <name> [intent] · $<name> [intent] · skill-owned slash aliases (e.g. /speckit.plan)");
63
+ console.log("Discovery dirs (later wins on name clash; JEO_SKILLS_DIR adds more):");
64
+ for (const d of skillDirs(cwd)) console.log(` ${d}`);
65
+ console.log("");
66
+ }
67
+ return;
68
+ }
69
+
70
+ let name: string;
71
+ if (command === "read") {
72
+ name = cleanArgs[1];
73
+ if (!name) {
74
+ console.log("Error: Missing skill name for 'read' command.");
25
75
  process.exitCode = 1;
26
76
  return;
27
77
  }
28
- console.log(formatSkill(skill));
78
+ } else {
79
+ name = command;
80
+ }
81
+
82
+ const skill = getSkillFrom(skills, name);
83
+ if (!skill) {
84
+ const knownNames = skills.map(s => s.name);
85
+ const suggestions = suggestSkills(name, knownNames);
86
+ const hint = suggestions.length ? ` Did you mean: ${suggestions.join(", ")}?` : "";
87
+ console.log(`Unknown skill: ${name}.${hint}\nAvailable: ${knownNames.join(", ")}`);
88
+ process.exitCode = 1;
29
89
  return;
30
90
  }
31
91
 
32
- console.log("\n=== joc skills ===");
33
- console.log("Bundled workflow skills (run 'joc skills <name>' for details, --write to export):\n");
34
- for (const s of SKILLS) {
35
- console.log(` ${s.name.padEnd(16)} ${s.summary}`);
92
+ if (isJson) {
93
+ const content = skill.raw || `# ${skill.name}\n\n${formatSkill(skill)}\n`;
94
+ console.log(JSON.stringify({ name: skill.name, content }, null, 2));
95
+ } else {
96
+ console.log(formatSkill(skill));
36
97
  }
37
- console.log("");
38
98
  }
@@ -0,0 +1,198 @@
1
+ import * as path from "node:path";
2
+ import {
3
+ readWorkflowState,
4
+ writeWorkflowState,
5
+ clearWorkflowState,
6
+ getLocalJeoDir,
7
+ type WorkflowState,
8
+ } from "../agent/state";
9
+
10
+ export interface ExtendedWorkflowState extends WorkflowState {
11
+ handoff_from?: string;
12
+ }
13
+
14
+ const allowedSkills = ["deep-interview", "ralplan", "team", "ultragoal"] as const;
15
+ type Skill = typeof allowedSkills[number];
16
+
17
+ function isSkill(val: string): val is Skill {
18
+ return allowedSkills.includes(val as Skill);
19
+ }
20
+
21
+ const allowedVerbs = ["read", "write", "clear", "handoff"] as const;
22
+ type Verb = typeof allowedVerbs[number];
23
+
24
+ function isVerb(val: string): val is Verb {
25
+ return allowedVerbs.includes(val as Verb);
26
+ }
27
+
28
+ function printUsage(): void {
29
+ console.log("Usage:");
30
+ console.log(" jeo state <skill> read [--json]");
31
+ console.log(" jeo state <skill> write --input '<json>' [--json]");
32
+ console.log(" jeo state <skill> clear");
33
+ console.log(" jeo state <skill> handoff --to <skill> [--json]");
34
+ console.log("");
35
+ console.log("Skills:");
36
+ console.log(" deep-interview, ralplan, team, ultragoal");
37
+ console.log("");
38
+ console.log("Verbs:");
39
+ console.log(" read, write, clear, handoff");
40
+ }
41
+
42
+ export async function runStateCommand(args: string[] = []): Promise<void> {
43
+ const cwd = process.cwd();
44
+
45
+ const isHelp = args.includes("--help") || args.includes("-h");
46
+ if (isHelp) {
47
+ printUsage();
48
+ process.exitCode = 0;
49
+ return;
50
+ }
51
+
52
+ if (args.length < 2) {
53
+ printUsage();
54
+ process.exitCode = 1;
55
+ return;
56
+ }
57
+
58
+ const skill = args[0];
59
+ const verb = args[1];
60
+
61
+ if (!isSkill(skill) || !isVerb(verb)) {
62
+ printUsage();
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+
67
+ const isJson = args.includes("--json");
68
+
69
+ if (verb === "read") {
70
+ const state = await readWorkflowState(skill, cwd);
71
+ if (!state) {
72
+ console.log(`No state found for skill: ${skill}`);
73
+ return;
74
+ }
75
+ if (isJson) {
76
+ console.log(JSON.stringify(state, null, 2));
77
+ } else {
78
+ const statePath = path.join(getLocalJeoDir(cwd), "state", `${skill}-state.json`);
79
+ console.log(`Skill: ${state.skill}`);
80
+ console.log(`Current Phase: ${state.current_phase}`);
81
+ console.log(`File Path: ${statePath}`);
82
+ console.log("Details:");
83
+ for (const [key, val] of Object.entries(state)) {
84
+ if (key !== "skill" && key !== "current_phase") {
85
+ console.log(` ${key}: ${typeof val === "object" ? JSON.stringify(val) : val}`);
86
+ }
87
+ }
88
+ }
89
+ return;
90
+ }
91
+
92
+ if (verb === "write") {
93
+ let inputJson: string | null = null;
94
+ for (let i = 2; i < args.length; i++) {
95
+ if (args[i] === "--input") {
96
+ inputJson = args[i + 1] || null;
97
+ i++;
98
+ } else if (args[i].startsWith("--input=")) {
99
+ inputJson = args[i].substring("--input=".length);
100
+ }
101
+ }
102
+
103
+ if (inputJson === null) {
104
+ console.log("[ERROR] --input option is required for write verb.");
105
+ process.exitCode = 1;
106
+ return;
107
+ }
108
+
109
+ let parsed: any;
110
+ try {
111
+ parsed = JSON.parse(inputJson);
112
+ } catch (err) {
113
+ console.log(`[ERROR] Malformed JSON input: ${(err as Error).message}`);
114
+ process.exitCode = 1;
115
+ return;
116
+ }
117
+
118
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
119
+ console.log("[ERROR] Input must be a valid JSON object.");
120
+ process.exitCode = 1;
121
+ return;
122
+ }
123
+
124
+ const existing = await readWorkflowState(skill, cwd);
125
+ const merged: WorkflowState = {
126
+ active: false,
127
+ current_phase: "",
128
+ ...existing,
129
+ ...parsed,
130
+ skill,
131
+ };
132
+
133
+ const statePath = await writeWorkflowState(skill, merged, cwd);
134
+ if (isJson) {
135
+ console.log(`State path: ${statePath}`);
136
+ console.log(JSON.stringify(merged, null, 2));
137
+ } else {
138
+ console.log(`State path: ${statePath}`);
139
+ }
140
+ return;
141
+ }
142
+
143
+ if (verb === "clear") {
144
+ await clearWorkflowState(skill, cwd);
145
+ console.log(`Cleared state for skill: ${skill}`);
146
+ return;
147
+ }
148
+
149
+ if (verb === "handoff") {
150
+ let toSkill: string | null = null;
151
+ for (let i = 2; i < args.length; i++) {
152
+ if (args[i] === "--to") {
153
+ toSkill = args[i + 1] || null;
154
+ i++;
155
+ } else if (args[i].startsWith("--to=")) {
156
+ toSkill = args[i].substring("--to=".length);
157
+ }
158
+ }
159
+
160
+ if (!toSkill || !isSkill(toSkill)) {
161
+ console.log(`[ERROR] Target skill must be specified with --to <skill>. Allowed skills: ${allowedSkills.join(", ")}`);
162
+ process.exitCode = 1;
163
+ return;
164
+ }
165
+
166
+ const sourceState = await readWorkflowState(skill, cwd) || {
167
+ active: false,
168
+ current_phase: "",
169
+ skill,
170
+ };
171
+ sourceState.current_phase = "handoff";
172
+ const sourcePath = await writeWorkflowState(skill, sourceState, cwd);
173
+
174
+ const targetState = await readWorkflowState(toSkill, cwd) || {
175
+ active: false,
176
+ current_phase: "",
177
+ skill: toSkill,
178
+ };
179
+ const updatedTarget: ExtendedWorkflowState = {
180
+ ...targetState,
181
+ active: true,
182
+ handoff_from: skill,
183
+ };
184
+ const targetPath = await writeWorkflowState(toSkill, updatedTarget, cwd);
185
+
186
+ if (isJson) {
187
+ console.log(JSON.stringify({
188
+ source: { path: sourcePath, state: sourceState },
189
+ target: { path: targetPath, state: updatedTarget }
190
+ }, null, 2));
191
+ } else {
192
+ console.log(`Handoff completed.`);
193
+ console.log(`Source (${skill}) state updated: ${sourcePath}`);
194
+ console.log(`Target (${toSkill}) state updated: ${targetPath}`);
195
+ }
196
+ return;
197
+ }
198
+ }
@@ -0,0 +1,84 @@
1
+ import * as fs from "node:fs/promises";
2
+ import * as path from "node:path";
3
+ import chalk from "chalk";
4
+
5
+ export async function runStatusCommand(): Promise<void> {
6
+ const cwd = process.cwd();
7
+ const logPath = path.join(cwd, "logs", "evolution-log.json");
8
+ const perfPath = path.join(cwd, ".jeo", "state", "performance-metrics.json");
9
+ const planPath = path.join(cwd, ".specify", "plan.md");
10
+
11
+ console.log(chalk.bold("\n=== jeo Core Engine Status ==="));
12
+
13
+ try {
14
+ const planContent = await fs.readFile(planPath, "utf-8");
15
+ const tasks = planContent.match(/- \[[x ]\] .+/g) || [];
16
+ const completedTasks = tasks.filter(t => t.startsWith("- [x]")).length;
17
+ const totalTasks = tasks.length;
18
+
19
+ if (totalTasks > 0) {
20
+ const percentage = (completedTasks / totalTasks) * 100;
21
+ const barWidth = 30;
22
+ const filledWidth = Math.round((completedTasks / totalTasks) * barWidth);
23
+ const bar = chalk.green("█").repeat(filledWidth) + chalk.gray("░").repeat(barWidth - filledWidth);
24
+
25
+ console.log(chalk.bold("Overall Project Progress: [" + bar + "] " + percentage.toFixed(1) + "%"));
26
+ console.log(chalk.dim("(" + completedTasks + "/" + totalTasks + " tasks completed from plan.md)\n"));
27
+ }
28
+ } catch (e) {}
29
+
30
+ try {
31
+ const content = await fs.readFile(logPath, "utf-8");
32
+ const logs = JSON.parse(content);
33
+
34
+ if (logs.length === 0) {
35
+ console.log("No evolution history found.");
36
+ } else {
37
+ const activeTask = logs.find((l: any) => l.status === "in_progress");
38
+ if (activeTask) {
39
+ console.log(chalk.yellow.bold("▶ ACTIVE EVOLUTION: ") + chalk.cyan(activeTask.target));
40
+ if (activeTask.stage) {
41
+ console.log(chalk.magenta(" Stage: ") + chalk.bold(activeTask.stage.toUpperCase()));
42
+ }
43
+ console.log(chalk.dim(" Started: " + activeTask.timestamp + "\n"));
44
+ }
45
+
46
+ console.log(chalk.bold("Recent Evolution Turns:"));
47
+ const recent = logs.slice(-5).reverse();
48
+ for (const entry of recent) {
49
+ const statusColor = entry.status === "success" ? chalk.green : (entry.status === "failed" ? chalk.red : chalk.yellow);
50
+ const stageLabel = entry.stage ? ` [${entry.stage.toUpperCase()}]` : "";
51
+ console.log("- " + chalk.dim("[" + new Date(entry.timestamp).toLocaleTimeString() + "]") + stageLabel.padEnd(16) + " " + statusColor(entry.status.toUpperCase().padEnd(11)) + " " + chalk.cyan(entry.target));
52
+ }
53
+ console.log("");
54
+ }
55
+ } catch (e) {
56
+ console.log("Status: Idle (No evolution logs)");
57
+ }
58
+
59
+ console.log(chalk.bold("=== Engine Performance Metrics ==="));
60
+ try {
61
+ const perfPathAlt = path.join(cwd, "logs", "performance-metrics.json");
62
+ let perfContent = "";
63
+ try {
64
+ perfContent = await fs.readFile(perfPath, "utf-8");
65
+ } catch {
66
+ perfContent = await fs.readFile(perfPathAlt, "utf-8");
67
+ }
68
+ const metrics = JSON.parse(perfContent);
69
+ if (metrics.length > 0) {
70
+ const recent = metrics.slice(-50);
71
+ const avgDuration = recent.reduce((sum: number, m: any) => sum + m.duration, 0) / recent.length;
72
+ const successRate = (recent.filter((m: any) => m.success).length / recent.length) * 100;
73
+
74
+ console.log("Average Tool Time: " + chalk.magenta(avgDuration.toFixed(0) + "ms"));
75
+ const successColor = successRate > 90 ? chalk.green : chalk.yellow;
76
+ console.log("Tool Success Rate: " + successColor(successRate.toFixed(1) + "%"));
77
+ } else {
78
+ console.log("No performance metrics collected yet.");
79
+ }
80
+ } catch (e) {
81
+ console.log("Performance metrics unavailable.");
82
+ }
83
+ console.log("");
84
+ }