jeo-code 0.1.0 → 0.4.4
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.
- package/README.ja.md +160 -0
- package/README.ko.md +160 -0
- package/README.md +115 -297
- package/README.zh.md +160 -0
- package/package.json +11 -6
- package/scripts/install.sh +28 -28
- package/scripts/uninstall.sh +17 -15
- package/src/AGENTS.md +50 -0
- package/src/agent/AGENTS.md +49 -0
- package/src/agent/bash-fixups.ts +103 -0
- package/src/agent/compaction.ts +410 -19
- package/src/agent/config-schema.ts +119 -5
- package/src/agent/context-files.ts +314 -17
- package/src/agent/dev/AGENTS.md +36 -0
- package/src/agent/dev/advanced-analyzer.ts +12 -0
- package/src/agent/dev/evolution-bridge.ts +82 -0
- package/src/agent/dev/evolution-logger.ts +41 -0
- package/src/agent/dev/self-analysis.ts +64 -0
- package/src/agent/dev/self-improve.ts +24 -0
- package/src/agent/dev/spec-automation.ts +49 -0
- package/src/agent/engine.ts +804 -54
- package/src/agent/hooks.ts +273 -0
- package/src/agent/loop.ts +21 -1
- package/src/agent/memory.ts +201 -0
- package/src/agent/model-recency.ts +32 -0
- package/src/agent/output-minimizer.ts +108 -0
- package/src/agent/output-util.ts +64 -0
- package/src/agent/plan.ts +187 -0
- package/src/agent/seed.ts +52 -0
- package/src/agent/session.ts +235 -21
- package/src/agent/state.ts +286 -39
- package/src/agent/step-budget.ts +232 -0
- package/src/agent/subagents.ts +223 -26
- package/src/agent/task-tool.ts +272 -0
- package/src/agent/todo-tool.ts +87 -0
- package/src/agent/tokenizer.ts +117 -0
- package/src/agent/tool-registry.ts +54 -0
- package/src/agent/tools.ts +562 -103
- package/src/agent/web-search.ts +538 -0
- package/src/ai/AGENTS.md +44 -0
- package/src/ai/index.ts +1 -0
- package/src/ai/model-catalog-compat.ts +3 -1
- package/src/ai/model-catalog.ts +74 -9
- package/src/ai/model-discovery.ts +215 -17
- package/src/ai/model-manager.ts +346 -32
- package/src/ai/model-picker.ts +1 -1
- package/src/ai/model-registry.ts +4 -2
- package/src/ai/pricing.ts +84 -0
- package/src/ai/provider-registry.ts +23 -0
- package/src/ai/provider-status.ts +60 -16
- package/src/ai/providers/AGENTS.md +42 -0
- package/src/ai/providers/anthropic.ts +250 -31
- package/src/ai/providers/antigravity.ts +219 -0
- package/src/ai/providers/errors.ts +15 -1
- package/src/ai/providers/gemini.ts +196 -13
- package/src/ai/providers/ollama.ts +37 -7
- package/src/ai/providers/openai-responses.ts +173 -0
- package/src/ai/providers/openai.ts +64 -12
- package/src/ai/sse.ts +4 -1
- package/src/ai/types.ts +18 -1
- package/src/auth/AGENTS.md +41 -0
- package/src/auth/callback-server.ts +6 -1
- package/src/auth/flows/AGENTS.md +32 -0
- package/src/auth/flows/antigravity.ts +151 -0
- package/src/auth/flows/google-project.ts +190 -0
- package/src/auth/flows/google.ts +39 -18
- package/src/auth/flows/index.ts +15 -5
- package/src/auth/flows/openai.ts +2 -2
- package/src/auth/oauth.ts +8 -0
- package/src/auth/refresh.ts +44 -27
- package/src/auth/storage.ts +149 -26
- package/src/auth/types.ts +1 -1
- package/src/autopilot.ts +362 -0
- package/src/bun-imports.d.ts +4 -0
- package/src/cli/AGENTS.md +39 -0
- package/src/cli/runner.ts +148 -14
- package/src/cli.ts +13 -4
- package/src/commands/AGENTS.md +40 -0
- package/src/commands/approve.ts +62 -3
- package/src/commands/auth.ts +167 -25
- package/src/commands/chat.ts +37 -8
- package/src/commands/deep-interview.ts +633 -175
- package/src/commands/doctor.ts +84 -37
- package/src/commands/evolve-core.ts +18 -0
- package/src/commands/evolve.ts +2 -1
- package/src/commands/export.ts +176 -0
- package/src/commands/gjc.ts +52 -0
- package/src/commands/launch.ts +3549 -240
- package/src/commands/mcp.ts +3 -3
- package/src/commands/ooo-seed.ts +19 -0
- package/src/commands/ralplan.ts +253 -35
- package/src/commands/resume.ts +1 -1
- package/src/commands/session.ts +183 -0
- package/src/commands/setup-helpers.ts +10 -3
- package/src/commands/setup.ts +57 -16
- package/src/commands/skills.ts +78 -18
- package/src/commands/state.ts +198 -0
- package/src/commands/status.ts +84 -0
- package/src/commands/team.ts +340 -212
- package/src/commands/ultragoal.ts +122 -61
- package/src/commands/update.ts +244 -0
- package/src/ledger.ts +270 -0
- package/src/mcp/AGENTS.md +38 -0
- package/src/mcp/server.ts +115 -14
- package/src/mcp/tools.ts +42 -22
- package/src/md-modules.d.ts +4 -0
- package/src/prompts/AGENTS.md +41 -0
- package/src/prompts/agents/AGENTS.md +35 -0
- package/src/prompts/agents/architect.md +35 -0
- package/src/prompts/agents/critic.md +37 -0
- package/src/prompts/agents/executor.md +36 -0
- package/src/prompts/agents/planner.md +37 -0
- package/src/prompts/skills/AGENTS.md +36 -0
- package/src/prompts/skills/deep-dive/AGENTS.md +31 -0
- package/src/prompts/skills/deep-dive/SKILL.md +13 -0
- package/src/prompts/skills/deep-interview/AGENTS.md +31 -0
- package/src/prompts/skills/deep-interview/SKILL.md +12 -0
- package/src/prompts/skills/gjc/AGENTS.md +31 -0
- package/src/prompts/skills/gjc/SKILL.md +15 -0
- package/src/prompts/skills/ralplan/AGENTS.md +31 -0
- package/src/prompts/skills/ralplan/SKILL.md +11 -0
- package/src/prompts/skills/team/AGENTS.md +31 -0
- package/src/prompts/skills/team/SKILL.md +11 -0
- package/src/prompts/skills/ultragoal/AGENTS.md +31 -0
- package/src/prompts/skills/ultragoal/SKILL.md +11 -0
- package/src/skills/AGENTS.md +38 -0
- package/src/skills/catalog.ts +565 -31
- package/src/tui/AGENTS.md +43 -0
- package/src/tui/app.ts +1181 -92
- package/src/tui/components/AGENTS.md +42 -0
- package/src/tui/components/ascii-art.ts +257 -15
- package/src/tui/components/autocomplete.ts +98 -16
- package/src/tui/components/autopilot-status.ts +65 -0
- package/src/tui/components/category-index.ts +49 -0
- package/src/tui/components/code-view.ts +54 -11
- package/src/tui/components/color.ts +171 -2
- package/src/tui/components/config-panel.ts +82 -15
- package/src/tui/components/duration.ts +38 -0
- package/src/tui/components/evolution.ts +3 -3
- package/src/tui/components/footer.ts +91 -42
- package/src/tui/components/forge.ts +426 -31
- package/src/tui/components/hints.ts +54 -0
- package/src/tui/components/hud.ts +73 -0
- package/src/tui/components/index.ts +4 -0
- package/src/tui/components/input-box.ts +150 -0
- package/src/tui/components/layout.ts +11 -3
- package/src/tui/components/live-model-picker.ts +108 -0
- package/src/tui/components/markdown-table.ts +140 -0
- package/src/tui/components/markdown-text.ts +97 -0
- package/src/tui/components/meter.ts +4 -1
- package/src/tui/components/model-picker.ts +3 -2
- package/src/tui/components/provider-picker.ts +3 -2
- package/src/tui/components/section.ts +70 -0
- package/src/tui/components/select-list.ts +40 -10
- package/src/tui/components/skill-picker.ts +25 -0
- package/src/tui/components/slash.ts +244 -21
- package/src/tui/components/status.ts +272 -11
- package/src/tui/components/step-timeline.ts +218 -0
- package/src/tui/components/stream.ts +26 -9
- package/src/tui/components/themes.ts +212 -6
- package/src/tui/components/todo-card.ts +47 -0
- package/src/tui/components/tool-list.ts +58 -12
- package/src/tui/components/transcript.ts +120 -0
- package/src/tui/components/update-box.ts +31 -0
- package/src/tui/components/welcome.ts +162 -0
- package/src/tui/components/width.ts +163 -0
- package/src/tui/monitoring/AGENTS.md +31 -0
- package/src/tui/monitoring/hud-view.ts +55 -0
- package/src/tui/renderer.ts +112 -3
- package/src/tui/terminal.ts +40 -33
- package/src/util/AGENTS.md +39 -0
- package/src/util/clipboard-image.ts +118 -0
- package/src/util/env.ts +12 -0
- package/src/util/provider-error.ts +78 -0
- package/src/util/retry.ts +91 -6
- package/src/util/update-check.ts +64 -0
- package/src/commands/models.ts +0 -104
package/src/skills/catalog.ts
CHANGED
|
@@ -1,41 +1,55 @@
|
|
|
1
|
+
import deepInterviewSkillRaw from "../prompts/skills/deep-interview/SKILL.md" with { type: "text" };
|
|
2
|
+
import deepDiveSkillRaw from "../prompts/skills/deep-dive/SKILL.md" with { type: "text" };
|
|
3
|
+
import ralplanSkillRaw from "../prompts/skills/ralplan/SKILL.md" with { type: "text" };
|
|
4
|
+
import teamSkillRaw from "../prompts/skills/team/SKILL.md" with { type: "text" };
|
|
5
|
+
import ultragoalSkillRaw from "../prompts/skills/ultragoal/SKILL.md" with { type: "text" };
|
|
6
|
+
import gjcSkillRaw from "../prompts/skills/gjc/SKILL.md" with { type: "text" };
|
|
7
|
+
|
|
8
|
+
const MAX_SKILL_SUMMARY_CHARS = 180;
|
|
9
|
+
const MAX_SKILL_DETAILS_CHARS = 8_000;
|
|
10
|
+
const MAX_SKILLS_PROMPT_LINES = 40;
|
|
11
|
+
const MAX_SKILLS_PROMPT_CHARS = 6_000;
|
|
12
|
+
|
|
1
13
|
export interface SkillDoc {
|
|
2
14
|
name: string; // e.g. "deep-interview"
|
|
3
|
-
command: string; // e.g. "
|
|
15
|
+
command: string; // e.g. "jeo deep-interview \"<idea>\""
|
|
4
16
|
summary: string; // one line
|
|
5
17
|
whenToUse: string; // one line
|
|
6
18
|
details: string; // 2-5 lines of guidance
|
|
19
|
+
/** Slash aliases that invoke this skill directly, e.g. `/speckit.plan`. */
|
|
20
|
+
aliases?: string[];
|
|
21
|
+
/** Source SKILL.md path for discovered skills; absent for bundled skills. */
|
|
22
|
+
sourcePath?: string;
|
|
23
|
+
raw?: string;
|
|
7
24
|
}
|
|
8
25
|
|
|
9
26
|
export const SKILLS: SkillDoc[] = [
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
},
|
|
17
|
-
{
|
|
18
|
-
name: "ralplan",
|
|
19
|
-
command: "joc ralplan",
|
|
20
|
-
summary: "Planner/Architect/Critic blueprint from the seed.",
|
|
21
|
-
whenToUse: "When requirements are clear (e.g. from deep-interview) and you need a robust execution blueprint.",
|
|
22
|
-
details: "Executes a multi-agent critique and planning process to generate a structured implementation plan.\nCombines views from a Planner, an Architect, and a Critic to identify risks, define tasks, and specify files.\nSaves the blueprint for execution."
|
|
23
|
-
},
|
|
24
|
-
{
|
|
25
|
-
name: "team",
|
|
26
|
-
command: "joc team",
|
|
27
|
-
summary: "Per-task executor loop against the plan.",
|
|
28
|
-
whenToUse: "When you have a blueprint/plan and need to execute the concrete implementation tasks.",
|
|
29
|
-
details: "Coordinates execution of individual tasks defined in the blueprint.\nSpawns per-task executor subagents or loops to implement code changes.\nEnsures task-level isolation and tracks implementation status."
|
|
30
|
-
},
|
|
31
|
-
{
|
|
32
|
-
name: "ultragoal",
|
|
33
|
-
command: "joc ultragoal",
|
|
34
|
-
summary: "Verify acceptance criteria, write report.",
|
|
35
|
-
whenToUse: "When tasks are implemented and you need a final, high-level verification and summary report.",
|
|
36
|
-
details: "Verifies the implementation against the acceptance criteria specified in the plan.\nRuns checks, tests, or validations to ensure correctness.\nGenerates a final completion report outlining the changes and verification evidence."
|
|
37
|
-
}
|
|
27
|
+
parseSkillMarkdown("deep-interview", deepInterviewSkillRaw),
|
|
28
|
+
parseSkillMarkdown("deep-dive", deepDiveSkillRaw),
|
|
29
|
+
parseSkillMarkdown("ralplan", ralplanSkillRaw),
|
|
30
|
+
parseSkillMarkdown("team", teamSkillRaw),
|
|
31
|
+
parseSkillMarkdown("ultragoal", ultragoalSkillRaw),
|
|
32
|
+
parseSkillMarkdown("gjc", gjcSkillRaw),
|
|
38
33
|
];
|
|
34
|
+
export const BUILTIN_SKILL_NAMES = SKILLS.map(s => s.name.toLowerCase());
|
|
35
|
+
|
|
36
|
+
export function workflowSkillsForPrompt(skills?: SkillDoc[]): SkillDoc[] {
|
|
37
|
+
if (!skills) {
|
|
38
|
+
return SKILLS;
|
|
39
|
+
}
|
|
40
|
+
const result: SkillDoc[] = [];
|
|
41
|
+
const bundleNames = new Set(BUILTIN_SKILL_NAMES);
|
|
42
|
+
|
|
43
|
+
result.push(...SKILLS);
|
|
44
|
+
|
|
45
|
+
for (const s of skills) {
|
|
46
|
+
if (!bundleNames.has(s.name.toLowerCase())) {
|
|
47
|
+
result.push(s);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
|
|
40
54
|
export function getSkill(name: string): SkillDoc | undefined {
|
|
41
55
|
return SKILLS.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
@@ -46,16 +60,536 @@ export function skillNames(): string[] {
|
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
export function formatSkill(s: SkillDoc): string {
|
|
63
|
+
const aliases = skillSlashAliases(s);
|
|
49
64
|
return [
|
|
50
65
|
`Skill: ${s.name}`,
|
|
51
66
|
`Command: ${s.command}`,
|
|
67
|
+
aliases.length ? `Slash aliases: ${aliases.join(", ")}` : undefined,
|
|
52
68
|
`Summary: ${s.summary}`,
|
|
53
69
|
`When to use: ${s.whenToUse}`,
|
|
54
70
|
`Details:`,
|
|
55
71
|
s.details.split("\n").map(line => ` ${line}`).join("\n")
|
|
72
|
+
].filter(Boolean).join("\n");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function compactSkillExecutionBrief(skill: SkillDoc): string {
|
|
76
|
+
const aliases = skillSlashAliases(skill);
|
|
77
|
+
let details = skill.details;
|
|
78
|
+
if (details.length > 2400) {
|
|
79
|
+
details = details.slice(0, 2400) + "…";
|
|
80
|
+
}
|
|
81
|
+
return [
|
|
82
|
+
`Name: ${skill.name}`,
|
|
83
|
+
aliases.length ? `Slash aliases: ${aliases.join(", ")}` : undefined,
|
|
84
|
+
`Summary: ${skill.summary}`,
|
|
85
|
+
skill.whenToUse ? `When to use: ${skill.whenToUse}` : undefined,
|
|
86
|
+
"Guidance:",
|
|
87
|
+
...details.split("\n").map(line => ` ${line}`),
|
|
88
|
+
].filter(Boolean).join("\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Build the agent task that EXECUTES a skill's workflow (rather than merely echoing
|
|
93
|
+
* the doc or — as weak models do — calling a bogus tool named after the skill). The
|
|
94
|
+
* skill text is injected as GUIDANCE and the agent is told to use its real tools.
|
|
95
|
+
*/
|
|
96
|
+
export function buildSkillTask(skill: SkillDoc, intent: string, invokedAs?: string): string {
|
|
97
|
+
const requested = invokedAs ? `Invoked as: ${invokedAs}\n` : "";
|
|
98
|
+
return [
|
|
99
|
+
`You are now executing the "${skill.name}" workflow skill in this repository.`,
|
|
100
|
+
`IMPORTANT: this skill is GUIDANCE for you — it is NOT a callable tool. Do NOT emit a tool call named "${skill.name}". Use your real tools (read, write, edit, bash, find, search, ls, task, todo) to carry out the work, then call done with a short summary.`,
|
|
101
|
+
`You must never quote or recite the guidance text as your reply; the done reason must describe actual work/outcome.`,
|
|
102
|
+
"",
|
|
103
|
+
`<skill_guidance name="${skill.name}">`,
|
|
104
|
+
compactSkillExecutionBrief(skill),
|
|
105
|
+
`</skill_guidance>`,
|
|
106
|
+
"",
|
|
107
|
+
requested +
|
|
108
|
+
(intent
|
|
109
|
+
? `User intent: ${intent}`
|
|
110
|
+
: "Carry out this skill's workflow now. If it needs a concrete target you weren't given, make a reasonable assumption or ask the user via done."),
|
|
56
111
|
].join("\n");
|
|
57
112
|
}
|
|
113
|
+
export function skillsPromptSection(skills: SkillDoc[] = SKILLS): string {
|
|
114
|
+
const bundleSkills: SkillDoc[] = [];
|
|
115
|
+
const configuredSkills: SkillDoc[] = [];
|
|
116
|
+
|
|
117
|
+
const bundleNames = new Set(BUILTIN_SKILL_NAMES);
|
|
118
|
+
for (const s of skills) {
|
|
119
|
+
if (bundleNames.has(s.name.toLowerCase())) {
|
|
120
|
+
bundleSkills.push(s);
|
|
121
|
+
} else {
|
|
122
|
+
configuredSkills.push(s);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const lines: string[] = [];
|
|
127
|
+
let used = 0;
|
|
128
|
+
|
|
129
|
+
function tryAppend(line: string): boolean {
|
|
130
|
+
if (lines.length >= MAX_SKILLS_PROMPT_LINES) return false;
|
|
131
|
+
if (used + line.length + 1 > MAX_SKILLS_PROMPT_CHARS) return false;
|
|
132
|
+
lines.push(line);
|
|
133
|
+
used += line.length + 1;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 1. Bundled workflow skills
|
|
138
|
+
if (bundleSkills.length > 0) {
|
|
139
|
+
if (tryAppend("Bundled workflow skills:")) {
|
|
140
|
+
for (const [i, s] of bundleSkills.entries()) {
|
|
141
|
+
const aliases = skillSlashAliases(s);
|
|
142
|
+
const line = `- ${s.name}${aliases.length ? ` (${aliases.join(", ")})` : ""} — ${s.summary}`;
|
|
143
|
+
if (!tryAppend(line)) {
|
|
144
|
+
const remaining = bundleSkills.length - i;
|
|
145
|
+
lines.push(`- … ${remaining} more skill(s) omitted for brevity`);
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
lines.push(`- … ${bundleSkills.length} more skill(s) omitted for brevity`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2. Configured skills
|
|
155
|
+
if (configuredSkills.length > 0) {
|
|
156
|
+
const isBudgetFull = lines.length >= MAX_SKILLS_PROMPT_LINES || used >= MAX_SKILLS_PROMPT_CHARS;
|
|
157
|
+
if (isBudgetFull) {
|
|
158
|
+
lines.push(`- … ${configuredSkills.length} more skill(s) omitted for brevity`);
|
|
159
|
+
} else {
|
|
160
|
+
const spaceAdded = tryAppend("");
|
|
161
|
+
const titleAdded = tryAppend("Configured skills:");
|
|
162
|
+
|
|
163
|
+
if (titleAdded) {
|
|
164
|
+
for (const [i, s] of configuredSkills.entries()) {
|
|
165
|
+
const aliases = skillSlashAliases(s);
|
|
166
|
+
const line = `- ${s.name}${aliases.length ? ` (${aliases.join(", ")})` : ""} — ${s.summary}`;
|
|
167
|
+
if (!tryAppend(line)) {
|
|
168
|
+
const remaining = configuredSkills.length - i;
|
|
169
|
+
lines.push(`- … ${remaining} more skill(s) omitted for brevity`);
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
if (spaceAdded && lines[lines.length - 1] === "") {
|
|
175
|
+
lines.pop();
|
|
176
|
+
}
|
|
177
|
+
lines.push(`- … ${configuredSkills.length} more skill(s) omitted for brevity`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return lines.join("\n");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
import * as fs from "node:fs/promises";
|
|
186
|
+
import * as path from "node:path";
|
|
187
|
+
import * as os from "node:os";
|
|
188
|
+
import { existsSync, statSync, readFileSync } from "node:fs";
|
|
189
|
+
import { jeoEnv } from "../util/env";
|
|
190
|
+
|
|
191
|
+
export function tryResolveSkillFromFilePath(filePath: string): SkillDoc | null {
|
|
192
|
+
try {
|
|
193
|
+
let targetPath = path.resolve(filePath);
|
|
194
|
+
if (!existsSync(targetPath)) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
const stat = statSync(targetPath);
|
|
198
|
+
if (stat.isDirectory()) {
|
|
199
|
+
const skillMd = path.join(targetPath, "SKILL.md");
|
|
200
|
+
if (existsSync(skillMd) && statSync(skillMd).isFile()) {
|
|
201
|
+
targetPath = skillMd;
|
|
202
|
+
} else {
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
} else if (!stat.isFile() || !targetPath.endsWith(".md")) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const content = readFileSync(targetPath, "utf-8");
|
|
210
|
+
// Determine a name for this skill
|
|
211
|
+
let skillName = path.basename(targetPath, ".md");
|
|
212
|
+
if (skillName.toLowerCase() === "skill" || skillName.toLowerCase() === "readme") {
|
|
213
|
+
// Use the directory name if the filename is generic
|
|
214
|
+
skillName = path.basename(path.dirname(targetPath));
|
|
215
|
+
}
|
|
216
|
+
const parsed = parseSkillMarkdown(skillName, content, { preferMetaName: true });
|
|
217
|
+
return isSupportedExternalSkill(parsed) ? parsed : null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const BUILTIN_SLASH_ALIASES = new Set([
|
|
223
|
+
"/help", "/clear", "/compact", "/model", "/fast", "/provider", "/logout",
|
|
224
|
+
"/agents", "/config", "/roles", "/thinking",
|
|
225
|
+
"/view", "/diff", "/find", "/search", "/sessions", "/skill", "/evolve",
|
|
226
|
+
"/exit", "/quit",
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
const RESERVED_SKILL_NAMES = new Set(
|
|
230
|
+
[...BUILTIN_SLASH_ALIASES].map(alias => alias.slice(1).toLowerCase())
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
function normalizeSlashAlias(raw: string): string | undefined {
|
|
234
|
+
const m = raw.trim().match(/^\/[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*$/);
|
|
235
|
+
if (!m) return undefined;
|
|
236
|
+
const alias = m[0];
|
|
237
|
+
return BUILTIN_SLASH_ALIASES.has(alias.toLowerCase()) ? undefined : alias;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function splitAliasHeader(value: string): string[] {
|
|
241
|
+
return value.split(/[,\s]+/).map(normalizeSlashAlias).filter((a): a is string => !!a);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function aliasOwner(alias: string): string {
|
|
245
|
+
return alias.slice(1).split(".", 1)[0]?.toLowerCase().replace(/[^a-z0-9]/g, "") ?? "";
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function skillAliasOwners(name: string): Set<string> {
|
|
249
|
+
const sanitize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
250
|
+
const owners = new Set([sanitize(name)]);
|
|
251
|
+
// Namespaced skills (e.g. "oh-my-claudecode:team") also own aliases for their
|
|
252
|
+
// last segment, so "/team" inside that skill's doc is still self-owned.
|
|
253
|
+
const last = name.split(/[:/]/).pop();
|
|
254
|
+
if (last) owners.add(sanitize(last));
|
|
255
|
+
owners.delete("");
|
|
256
|
+
return owners;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function inferSlashAliases(content: string, skillName: string): string[] {
|
|
260
|
+
const aliases: string[] = [];
|
|
261
|
+
const owners = skillAliasOwners(skillName);
|
|
262
|
+
const re = /(?:^|[\s`([{])((\/[A-Za-z][A-Za-z0-9_-]*(?:\.[A-Za-z][A-Za-z0-9_-]*)*))/g;
|
|
263
|
+
let match: RegExpExecArray | null;
|
|
264
|
+
while ((match = re.exec(content))) {
|
|
265
|
+
const alias = normalizeSlashAlias(match[1] ?? "");
|
|
266
|
+
if (!alias || !owners.has(aliasOwner(alias))) continue;
|
|
267
|
+
if (!aliases.some(a => a.toLowerCase() === alias.toLowerCase())) aliases.push(alias);
|
|
268
|
+
}
|
|
269
|
+
return aliases;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function dedupeAliases(aliases: string[]): string[] {
|
|
273
|
+
const seen = new Set<string>();
|
|
274
|
+
const out: string[] = [];
|
|
275
|
+
for (const alias of aliases) {
|
|
276
|
+
const key = alias.toLowerCase();
|
|
277
|
+
if (seen.has(key)) continue;
|
|
278
|
+
seen.add(key);
|
|
279
|
+
out.push(alias);
|
|
280
|
+
}
|
|
281
|
+
return out;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export function skillSlashAliases(skill: SkillDoc): string[] {
|
|
285
|
+
return dedupeAliases(skill.aliases ?? []);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
/** Global + per-project skill-doc directories (user-configurable SKILL.md files).
|
|
290
|
+
* Ordered lowest → highest precedence (a later dir's skill overrides an earlier
|
|
291
|
+
* one with the same name): foreign-ecosystem roots first, jeo-native last.
|
|
292
|
+
* Covered install layouts:
|
|
293
|
+
* - Vercel `npx skills add [-g]` canonical store: `.agents/skills/` (project + ~)
|
|
294
|
+
* - Vercel agent-targeted installs (`-a claude-code`): `.claude/skills/` (project + ~)
|
|
295
|
+
* - jeo agent skills (self-contained `.jeo` namespace, gjc-structure parity):
|
|
296
|
+
* `<config>/agent/skills/` (+ project `<cwd>/.jeo/agent/skills/`)
|
|
297
|
+
* - jeo-native flat: `<config>/skills/`, `<cwd>/.jeo/skills/`, then `JEO_SKILLS_DIR`. */
|
|
298
|
+
export function skillDirs(cwd: string = process.cwd()): string[] {
|
|
299
|
+
// $HOME wins over os.homedir(): Bun caches the system home, so tests (and
|
|
300
|
+
// sandboxed runs) that re-point HOME would otherwise still scan the real one.
|
|
301
|
+
const userHome = process.env.HOME || os.homedir();
|
|
302
|
+
const home = jeoEnv("CONFIG_DIR") || path.join(userHome, ".jeo");
|
|
303
|
+
const configured = (jeoEnv("SKILLS_DIR") ?? "")
|
|
304
|
+
.split(path.delimiter)
|
|
305
|
+
.map(s => s.trim())
|
|
306
|
+
.filter(Boolean);
|
|
307
|
+
return [
|
|
308
|
+
path.join(userHome, ".claude", "skills"),
|
|
309
|
+
path.join(home, "agent", "skills"),
|
|
310
|
+
path.join(userHome, ".agents", "skills"),
|
|
311
|
+
path.join(home, "skills"),
|
|
312
|
+
path.join(cwd, ".claude", "skills"),
|
|
313
|
+
path.join(cwd, ".agents", "skills"),
|
|
314
|
+
path.join(cwd, ".jeo", "skills"),
|
|
315
|
+
path.join(cwd, ".jeo", "agent", "skills"),
|
|
316
|
+
...configured,
|
|
317
|
+
];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** A frontmatter `name:` usable as a skill identity: one bare token, no spaces. */
|
|
321
|
+
function sanitizeSkillName(raw: string | undefined): string | undefined {
|
|
322
|
+
const m = (raw ?? "").trim();
|
|
323
|
+
return /^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$/.test(m) ? m : undefined;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/** Parse a user skill markdown file into a SkillDoc. Recognizes both the documented
|
|
327
|
+
* `key: value` header grammar (summary / command / when[ to use] / use) AND the
|
|
328
|
+
* decorated form emitted by `jeo skills --write` (`Skill:` / `When to use:` /
|
|
329
|
+
* `Details:`), tolerating a leading `# title` and blank separators. Falls back to
|
|
330
|
+
* inferring the summary from the first body line. */
|
|
331
|
+
export function parseSkillMarkdown(name: string, content: string, opts?: { preferMetaName?: boolean }): SkillDoc {
|
|
332
|
+
const meta: Record<string, string> = {};
|
|
333
|
+
const lines = content.split(/\r?\n/);
|
|
334
|
+
const HEADER = /^(summary|command|when to use|when|whentouse|use|skill|alias|aliases|slash|slashes)\s*:\s*(.*)$/i;
|
|
335
|
+
let idx = 0;
|
|
336
|
+
// YAML-style frontmatter (the standard agent SKILL.md format): a `---` … `---`
|
|
337
|
+
// block at the very top. `description:` maps to the summary so real skill files
|
|
338
|
+
// never surface a literal "---" summary into the prompt or /skill list.
|
|
339
|
+
while (idx < lines.length && lines[idx]!.trim() === "") idx++;
|
|
340
|
+
if (idx < lines.length && lines[idx]!.trim() === "---") {
|
|
341
|
+
idx++;
|
|
342
|
+
for (; idx < lines.length && lines[idx]!.trim() !== "---"; idx++) {
|
|
343
|
+
const fm = lines[idx]!.match(/^([A-Za-z][\w-]*)\s*:\s*(.*)$/);
|
|
344
|
+
if (!fm) continue;
|
|
345
|
+
const key = fm[1]!.toLowerCase().replace(/[\s_-]+/g, "");
|
|
346
|
+
let value = fm[2]!.trim().replace(/^["']|["']$/g, "");
|
|
347
|
+
// YAML block scalar indicator `>` / `|` (with optional chomping `+`/`-`). Also fold in
|
|
348
|
+
// the invalid-but-ubiquitous lead-in form real SKILL.md files use, e.g.
|
|
349
|
+
// `description: Use this skill when >` followed by an indented continuation block —
|
|
350
|
+
// otherwise the summary is the truncated nonsense "Use this skill when >".
|
|
351
|
+
// Require whitespace before the indicator (or the whole value) so a description that
|
|
352
|
+
// merely ENDS in `>` (e.g. "returns <T>") is left intact.
|
|
353
|
+
if (/(?:^|\s)[>|][+-]?$/.test(value)) {
|
|
354
|
+
const lead = value.replace(/\s*[>|][+-]?$/, "").trim();
|
|
355
|
+
const block: string[] = [];
|
|
356
|
+
for (idx++; idx < lines.length && /^\s+/.test(lines[idx] ?? ""); idx++) block.push(lines[idx]!.trim());
|
|
357
|
+
idx--;
|
|
358
|
+
value = [lead, block.join(" ")].filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
|
|
359
|
+
}
|
|
360
|
+
meta[key] = value;
|
|
361
|
+
}
|
|
362
|
+
if (idx < lines.length) idx++; // consume the closing ---
|
|
363
|
+
}
|
|
364
|
+
// Skip leading blank lines and a single leading markdown title.
|
|
365
|
+
while (idx < lines.length && lines[idx]!.trim() === "") idx++;
|
|
366
|
+
if (idx < lines.length && lines[idx]!.startsWith("# ")) {
|
|
367
|
+
idx++;
|
|
368
|
+
while (idx < lines.length && lines[idx]!.trim() === "") idx++;
|
|
369
|
+
}
|
|
370
|
+
// Parse a leading header block (tolerating blank separators); `Details:` (or the
|
|
371
|
+
// first free line) begins the body.
|
|
372
|
+
let detailsBlock: string[] | null = null;
|
|
373
|
+
for (; idx < lines.length; idx++) {
|
|
374
|
+
const raw = lines[idx]!;
|
|
375
|
+
if (raw.trim() === "") continue;
|
|
376
|
+
const dm = raw.match(/^details\s*:\s*(.*)$/i);
|
|
377
|
+
if (dm) {
|
|
378
|
+
const inline = dm[1]!.trim();
|
|
379
|
+
detailsBlock = inline ? [inline] : [];
|
|
380
|
+
for (idx++; idx < lines.length; idx++) detailsBlock.push(lines[idx]!.replace(/^ {2}/, ""));
|
|
381
|
+
break;
|
|
382
|
+
}
|
|
383
|
+
const m = raw.match(HEADER);
|
|
384
|
+
if (m) { meta[m[1]!.toLowerCase().replace(/\s+/g, "")] = m[2]!.trim(); continue; }
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
const body = (detailsBlock ? detailsBlock.join("\n") : lines.slice(idx).join("\n")).trim();
|
|
388
|
+
const firstLine = body.split("\n").find(l => l.trim())?.trim() ?? "";
|
|
389
|
+
const explicitAliases = [
|
|
390
|
+
...splitAliasHeader(meta.alias ?? ""),
|
|
391
|
+
...splitAliasHeader(meta.aliases ?? ""),
|
|
392
|
+
...splitAliasHeader(meta.slash ?? ""),
|
|
393
|
+
...splitAliasHeader(meta.slashes ?? ""),
|
|
394
|
+
];
|
|
395
|
+
const rawSummary = meta.summary ?? meta.description ?? firstLine;
|
|
396
|
+
const summary = rawSummary ? (rawSummary.length > MAX_SKILL_SUMMARY_CHARS ? rawSummary.slice(0, MAX_SKILL_SUMMARY_CHARS - 1) + "…" : rawSummary) : name;
|
|
397
|
+
const details = body
|
|
398
|
+
? (body.length > MAX_SKILL_DETAILS_CHARS ? body.slice(0, MAX_SKILL_DETAILS_CHARS - 1) + "…" : body)
|
|
399
|
+
: "(no details)";
|
|
400
|
+
// External (Vercel/agent-skills standard) SKILL.md files carry their identity in
|
|
401
|
+
// the frontmatter `name:`; honor it over the directory/file name when the caller
|
|
402
|
+
// opts in — bundled skills keep their fixed constructor names.
|
|
403
|
+
const docName = (opts?.preferMetaName ? sanitizeSkillName(meta.name) : undefined) ?? name;
|
|
404
|
+
return {
|
|
405
|
+
name: docName,
|
|
406
|
+
command: meta.command ?? `/skill ${docName}`,
|
|
407
|
+
summary,
|
|
408
|
+
whenToUse: meta.whentouse ?? meta.when ?? meta.use ?? "",
|
|
409
|
+
details,
|
|
410
|
+
aliases: dedupeAliases([...explicitAliases, ...inferSlashAliases(content, docName)]),
|
|
411
|
+
raw: content,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function isSupportedExternalSkill(doc: SkillDoc): boolean {
|
|
416
|
+
return !RESERVED_SKILL_NAMES.has(doc.name.toLowerCase());
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Bundled skills merged with user skill docs from {@link skillDirs} (user overrides by name). */
|
|
420
|
+
export async function loadSkills(cwd: string = process.cwd()): Promise<SkillDoc[]> {
|
|
421
|
+
const byName = new Map<string, SkillDoc>(SKILLS.map(s => [s.name.toLowerCase(), s]));
|
|
422
|
+
for (const dir of skillDirs(cwd)) {
|
|
423
|
+
let entries: import("node:fs").Dirent[] = [];
|
|
424
|
+
try { entries = await fs.readdir(dir, { withFileTypes: true }); } catch { continue; }
|
|
425
|
+
for (const entry of entries) {
|
|
426
|
+
if (entry.name.startsWith(".")) continue;
|
|
427
|
+
let isFile = entry.isFile();
|
|
428
|
+
let isDir = entry.isDirectory();
|
|
429
|
+
// Vercel `npx skills add` links agent dirs (.claude/skills/x) to its canonical
|
|
430
|
+
// store — a symlinked skill reports NEITHER isFile nor isDirectory on the
|
|
431
|
+
// Dirent, which silently dropped every linked skill. Follow the link.
|
|
432
|
+
if (entry.isSymbolicLink()) {
|
|
433
|
+
try {
|
|
434
|
+
const st = await fs.stat(path.join(dir, entry.name));
|
|
435
|
+
isFile = st.isFile();
|
|
436
|
+
isDir = st.isDirectory();
|
|
437
|
+
} catch { continue; /* broken symlink */ }
|
|
438
|
+
}
|
|
439
|
+
if (isFile && entry.name.endsWith(".md")) {
|
|
440
|
+
const nm = entry.name.slice(0, -3);
|
|
441
|
+
try {
|
|
442
|
+
const parsed = parseSkillMarkdown(nm, await fs.readFile(path.join(dir, entry.name), "utf-8"), { preferMetaName: true });
|
|
443
|
+
parsed.sourcePath = path.join(dir, entry.name);
|
|
444
|
+
if (isSupportedExternalSkill(parsed)) byName.set(parsed.name.toLowerCase(), parsed);
|
|
445
|
+
} catch { /* skip unreadable file */ }
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (isDir) {
|
|
449
|
+
const skillPath = path.join(dir, entry.name, "SKILL.md");
|
|
450
|
+
try {
|
|
451
|
+
const parsed = parseSkillMarkdown(entry.name, await fs.readFile(skillPath, "utf-8"), { preferMetaName: true });
|
|
452
|
+
parsed.sourcePath = skillPath;
|
|
453
|
+
if (isSupportedExternalSkill(parsed)) byName.set(parsed.name.toLowerCase(), parsed);
|
|
454
|
+
} catch { /* skip dirs without SKILL.md or unreadable files */ }
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return [...byName.values()];
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/** gjc-style skill-invocation card body (the `[skill]` block shown in the TUI
|
|
462
|
+
* when `$name`//skill runs): name, resolved SKILL.md path (or the bundled
|
|
463
|
+
* module path), and the prompt size actually injected. Pure — testable. */
|
|
464
|
+
export function skillInvocationCard(skill: SkillDoc): string[] {
|
|
465
|
+
const promptLines = (skill.raw ?? skill.details ?? "").split("\n").filter(l => l.trim().length > 0).length;
|
|
466
|
+
// jeo-ref tree-connector detail: the skill name leads, resolved metadata hangs
|
|
467
|
+
// off ├─/└─ connectors so the card scans like the reference's Skill panel.
|
|
468
|
+
return [
|
|
469
|
+
`Skill: ${skill.name}`,
|
|
470
|
+
`├─ path: ${skill.sourcePath ?? `(bundled) src/prompts/skills/${skill.name}/SKILL.md`}`,
|
|
471
|
+
`└─ prompt: ${promptLines} lines`,
|
|
472
|
+
];
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/** Case-insensitive lookup within a resolved skill list. */
|
|
476
|
+
export function getSkillFrom(skills: SkillDoc[], name: string): SkillDoc | undefined {
|
|
477
|
+
return skills.find(s => s.name.toLowerCase() === name.toLowerCase());
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** Case-insensitive lookup by direct slash alias, e.g. `/speckit.plan`. */
|
|
481
|
+
export function getSkillBySlash(skills: SkillDoc[], command: string): SkillDoc | undefined {
|
|
482
|
+
const q = command.toLowerCase();
|
|
483
|
+
return skills.find(s => skillSlashAliases(s).some(a => a.toLowerCase() === q));
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
export interface SkillInvocation {
|
|
487
|
+
skill: SkillDoc;
|
|
488
|
+
intent: string;
|
|
489
|
+
invokedAs?: string;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/** Parse only explicit skill invocations. Ambient mentions of skill names or slash
|
|
493
|
+
* aliases inside a broader prompt are deliberately ignored so pasted SKILL.md files
|
|
494
|
+
* cannot hijack an ordinary coding request. */
|
|
495
|
+
export function parseSkillInvocation(input: string, skills: SkillDoc[]): SkillInvocation | null {
|
|
496
|
+
const trimmed = input.trim();
|
|
497
|
+
if (!trimmed) return null;
|
|
498
|
+
|
|
499
|
+
const explicitEntrypoint = trimmed.startsWith("/skill:")
|
|
500
|
+
? "/skill:"
|
|
501
|
+
: (trimmed === "/skill" || trimmed.startsWith("/skill ")) ? "/skill" : "";
|
|
502
|
+
if (explicitEntrypoint) {
|
|
503
|
+
const rest = trimmed.substring(explicitEntrypoint.length).trim();
|
|
504
|
+
if (!rest) return null;
|
|
505
|
+
const [name, ...intentParts] = rest.split(/\s+/);
|
|
506
|
+
let skill = getSkillFrom(skills, name ?? "");
|
|
507
|
+
if (!skill && name) {
|
|
508
|
+
skill = tryResolveSkillFromFilePath(name) ?? undefined;
|
|
509
|
+
}
|
|
510
|
+
return skill ? { skill, intent: intentParts.join(" ").trim() } : null;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const command = trimmed.split(/\s+/, 1)[0] ?? "";
|
|
514
|
+
// Codex/gjc-style exact-name entrypoint: `$team [intent]` invokes the skill
|
|
515
|
+
// named "team" directly (case-insensitive). Only the FIRST token counts, and
|
|
516
|
+
// only when a skill with that exact name is loaded — `$HOME is what?` or any
|
|
517
|
+
// unknown `$word` falls through to the model as an ordinary prompt.
|
|
518
|
+
if (command.length > 1 && command.startsWith("$")) {
|
|
519
|
+
const dollarSkill = getSkillFrom(skills, command.slice(1));
|
|
520
|
+
if (dollarSkill) {
|
|
521
|
+
return { skill: dollarSkill, intent: trimmed.slice(command.length).trim(), invokedAs: command };
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
let skill = getSkillBySlash(skills, command);
|
|
525
|
+
if (!skill) {
|
|
526
|
+
if (command.startsWith("/") || command.startsWith(".") || command.includes("/")) {
|
|
527
|
+
const resolved = tryResolveSkillFromFilePath(command);
|
|
528
|
+
if (resolved) {
|
|
529
|
+
return { skill: resolved, intent: trimmed.slice(command.length).trim(), invokedAs: command };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
return skill ? { skill, intent: trimmed.slice(command.length).trim(), invokedAs: command } : null;
|
|
534
|
+
}
|
|
535
|
+
export function looksLikeSkillEcho(reply: string, skills: SkillDoc[]): boolean {
|
|
536
|
+
if (reply.length < 80) {
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
const lines = reply.split(/\r?\n/);
|
|
541
|
+
|
|
542
|
+
// Heuristic 1: Contains <skill_guidance or a line starting with "Skill: " AND a line starting with "When to use:"
|
|
543
|
+
if (reply.includes("<skill_guidance")) {
|
|
544
|
+
return true;
|
|
545
|
+
}
|
|
546
|
+
const hasSkillLine = lines.some(l => l.trim().startsWith("Skill: "));
|
|
547
|
+
const hasWhenToUseLine = lines.some(l => l.trim().startsWith("When to use:"));
|
|
548
|
+
if (hasSkillLine && hasWhenToUseLine) {
|
|
549
|
+
return true;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Heuristic 2: >= 3 reply lines are near-verbatim matches (trimmed, case-insensitive)
|
|
553
|
+
// of skill summary lines or of "- <name> — <summary>" lines that skillsPromptSection would emit
|
|
554
|
+
const targets = new Set<string>();
|
|
555
|
+
for (const s of skills) {
|
|
556
|
+
if (s.summary) {
|
|
557
|
+
targets.add(s.summary.trim().toLowerCase());
|
|
558
|
+
}
|
|
559
|
+
const aliases = skillSlashAliases(s);
|
|
560
|
+
const promptLine = `- ${s.name}${aliases.length ? ` (${aliases.join(", ")})` : ""} — ${s.summary}`;
|
|
561
|
+
targets.add(promptLine.trim().toLowerCase());
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let matchCount = 0;
|
|
565
|
+
for (const line of lines) {
|
|
566
|
+
const trimmedLine = line.trim().toLowerCase();
|
|
567
|
+
if (targets.has(trimmedLine)) {
|
|
568
|
+
matchCount++;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
if (matchCount >= 3) {
|
|
572
|
+
return true;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Heuristic 3: Contains a verbatim chunk (>= 160 consecutive chars) of any skill's details
|
|
576
|
+
// (only check first 50 skills, and only those with details length >= 160, using start/middle/end probes)
|
|
577
|
+
const checkedSkills = skills.slice(0, 50);
|
|
578
|
+
for (const s of checkedSkills) {
|
|
579
|
+
const details = s.details;
|
|
580
|
+
if (!details || details.length < 160) {
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
583
|
+
const len = details.length;
|
|
584
|
+
const startProbe = details.slice(0, 160);
|
|
585
|
+
const midStart = Math.floor((len - 160) / 2);
|
|
586
|
+
const midProbe = details.slice(midStart, midStart + 160);
|
|
587
|
+
const endProbe = details.slice(len - 160);
|
|
588
|
+
|
|
589
|
+
if (reply.includes(startProbe) || reply.includes(midProbe) || reply.includes(endProbe)) {
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
58
593
|
|
|
59
|
-
|
|
60
|
-
return SKILLS.map(s => `- ${s.name} — ${s.summary}`).join("\n");
|
|
594
|
+
return false;
|
|
61
595
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
<!-- Parent: ../AGENTS.md -->
|
|
2
|
+
<!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
|
|
3
|
+
|
|
4
|
+
# tui
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
Terminal User Interface logic, layout systems, and the differential renderer. Responsible for visualizing the agent's state, tools, and streams.
|
|
8
|
+
|
|
9
|
+
## Key Files
|
|
10
|
+
| File | Description |
|
|
11
|
+
|------|-------------|
|
|
12
|
+
| `app.ts` | High-level TUI orchestrator (`LaunchTui`) |
|
|
13
|
+
| `renderer.ts` | The differential, atomic terminal renderer (handles scrollback, resizing, DECSET 2026) |
|
|
14
|
+
| `terminal.ts` | Low-level ANSI escape codes and terminal size utilities |
|
|
15
|
+
|
|
16
|
+
## Subdirectories
|
|
17
|
+
| Directory | Purpose |
|
|
18
|
+
|-----------|---------|
|
|
19
|
+
| `components/` | Reusable UI widgets (footer, forge boxes, timeline, layouts) (see `components/AGENTS.md`) |
|
|
20
|
+
| `monitoring/` | Specialized HUD views (see `monitoring/AGENTS.md`) |
|
|
21
|
+
|
|
22
|
+
## For AI Agents
|
|
23
|
+
|
|
24
|
+
### Working In This Directory
|
|
25
|
+
- The differential renderer (`renderer.ts`) is critical for the "scrollback-friendly inline live turn" feature. DO NOT introduce full-screen clears (`\x1b[0J` mid-turn) as they flood tmux scrollback.
|
|
26
|
+
- Maintain visual parity with `gjc` design patterns (e.g., shadcn-inspired stage-grouped card layouts, muted card headers).
|
|
27
|
+
|
|
28
|
+
### Testing Requirements
|
|
29
|
+
- Test rendering logic using mocked stdout.
|
|
30
|
+
- Verify resize behavior and atomic flushes.
|
|
31
|
+
|
|
32
|
+
### Common Patterns
|
|
33
|
+
- Separation of UI from engine: `app.ts` listens to `AgentLoopEvents` but does not import engine loop directly.
|
|
34
|
+
|
|
35
|
+
## Dependencies
|
|
36
|
+
|
|
37
|
+
### Internal
|
|
38
|
+
- Driven by events from `src/agent/loop.ts`.
|
|
39
|
+
|
|
40
|
+
### External
|
|
41
|
+
- ANSI escape sequence management.
|
|
42
|
+
|
|
43
|
+
<!-- MANUAL: Any manually added notes below this line are preserved on regeneration -->
|