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.
- 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 +808 -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 +624 -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
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import * as fs from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
|
|
4
|
+
export const TOOL_SPILL_THRESHOLD = 32_000;
|
|
5
|
+
|
|
6
|
+
export function truncateToolOutput(output: string, limit = 8000): string {
|
|
7
|
+
if (output.length <= limit) return output;
|
|
8
|
+
const half = Math.floor(limit / 2);
|
|
9
|
+
return output.slice(0, half) + "\n\n... (elided " + (output.length - limit) + " chars) ...\n\n" + output.slice(-half);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function spillToolResult(tool: string, output: string, cwd: string): Promise<string> {
|
|
13
|
+
const artifactsDir = path.join(cwd, ".jeo", "artifacts");
|
|
14
|
+
await fs.mkdir(artifactsDir, { recursive: true });
|
|
15
|
+
|
|
16
|
+
const ts = new Date().toISOString().replace(/[:.]/g, "-");
|
|
17
|
+
const filename = "result-" + tool + "-" + ts + ".txt";
|
|
18
|
+
const fullPath = path.join(artifactsDir, filename);
|
|
19
|
+
|
|
20
|
+
await fs.writeFile(fullPath, output, "utf-8");
|
|
21
|
+
return path.relative(cwd, fullPath);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One tool execution's performance record (consumed by dev self-analysis). */
|
|
25
|
+
export interface PerfMetric {
|
|
26
|
+
tool: string;
|
|
27
|
+
duration: number;
|
|
28
|
+
success: boolean;
|
|
29
|
+
error?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Append a tool performance metric to `.jeo/state/performance-metrics.json`
|
|
33
|
+
* (bounded to the most recent 200 records). Best-effort: failures are ignored
|
|
34
|
+
* so metrics can never break an agent turn. */
|
|
35
|
+
export async function logPerformanceMetric(cwd: string, metric: PerfMetric): Promise<void> {
|
|
36
|
+
try {
|
|
37
|
+
const dir = path.join(cwd, ".jeo", "state");
|
|
38
|
+
const file = path.join(dir, "performance-metrics.json");
|
|
39
|
+
await fs.mkdir(dir, { recursive: true });
|
|
40
|
+
let records: PerfMetric[] = [];
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(await fs.readFile(file, "utf-8"));
|
|
43
|
+
if (Array.isArray(parsed)) records = parsed;
|
|
44
|
+
} catch { /* first write or corrupt file → start fresh */ }
|
|
45
|
+
records.push(metric);
|
|
46
|
+
if (records.length > 200) records = records.slice(-200);
|
|
47
|
+
await fs.writeFile(file, JSON.stringify(records), "utf-8");
|
|
48
|
+
} catch { /* best-effort */ }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Read spec-kit project context (`.specify/memory/constitution.md`) when the repo
|
|
52
|
+
* was initialized with GitHub spec-kit, so the agent honors the project
|
|
53
|
+
* constitution. Returns null when absent/unreadable; content is capped. */
|
|
54
|
+
export async function loadSpecKitContext(cwd: string): Promise<string | null> {
|
|
55
|
+
try {
|
|
56
|
+
const file = path.join(cwd, ".specify", "memory", "constitution.md");
|
|
57
|
+
const content = (await fs.readFile(file, "utf-8")).trim();
|
|
58
|
+
if (!content) return null;
|
|
59
|
+
const capped = content.length > 4000 ? content.slice(0, 4000) + "…" : content;
|
|
60
|
+
return "\n\n<spec-kit-constitution>\n" + capped + "\n</spec-kit-constitution>";
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plan-document schema + minimal YAML parser for jeo workflow plans.
|
|
3
|
+
* Used by both `jeo team` (execution) and `jeo ralplan` (plan generation/validation).
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
/** Dependency-shaped keys the SERIAL executor cannot honor: `jeo team` runs steps
|
|
8
|
+
* in array order, so a plan that EXPRESSES ordering constraints must fail loudly
|
|
9
|
+
* instead of silently creating the illusion they are enforced (round-10 LOW). */
|
|
10
|
+
const UNSUPPORTED_DEP_KEYS = ["depends_on", "dependsOn", "after", "needs", "requires", "dependencies"] as const;
|
|
11
|
+
|
|
12
|
+
export const StepSchema = z.object({
|
|
13
|
+
name: z.string(),
|
|
14
|
+
/** Optional subagent role for this step (executor/planner/architect/critic). */
|
|
15
|
+
role: z.string().optional(),
|
|
16
|
+
}).passthrough().superRefine((step, ctx) => {
|
|
17
|
+
for (const key of UNSUPPORTED_DEP_KEYS) {
|
|
18
|
+
if (key in step) {
|
|
19
|
+
ctx.addIssue({
|
|
20
|
+
code: z.ZodIssueCode.custom,
|
|
21
|
+
message: `step "${String((step as Record<string, unknown>).name ?? "?")}" declares "${key}", but jeo team executes steps strictly in array order and does NOT honor dependency metadata — reorder the steps array instead and remove "${key}".`,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
export const PlanSchema = z.object({
|
|
28
|
+
name: z.string().optional(),
|
|
29
|
+
steps: z.array(StepSchema).min(1),
|
|
30
|
+
}).passthrough();
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Tolerate common planning-model deviations so a valid-enough plan still executes:
|
|
34
|
+
* a top-level list of tasks, a `tasks:` alias for `steps:`, bare-string tasks, and
|
|
35
|
+
* step name under `task`/`title`/`description`/`step`.
|
|
36
|
+
*/
|
|
37
|
+
export function normalizePlanShape(raw: any): any {
|
|
38
|
+
let plan = raw;
|
|
39
|
+
if (Array.isArray(plan)) plan = { steps: plan };
|
|
40
|
+
if (plan && typeof plan === "object" && !Array.isArray(plan)) {
|
|
41
|
+
if (!Array.isArray(plan.steps) && Array.isArray(plan.tasks)) plan = { ...plan, steps: plan.tasks };
|
|
42
|
+
if (Array.isArray(plan.steps)) {
|
|
43
|
+
plan = {
|
|
44
|
+
...plan,
|
|
45
|
+
steps: plan.steps.map((s: any) =>
|
|
46
|
+
typeof s === "string"
|
|
47
|
+
? { name: s }
|
|
48
|
+
: s && typeof s === "object" && !s.name
|
|
49
|
+
? { ...s, name: s.task ?? s.title ?? s.description ?? s.step ?? "" }
|
|
50
|
+
: s,
|
|
51
|
+
).filter((s: any) => s && typeof s.name === "string" && s.name.trim() !== ""),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return plan;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function parseValue(v: string): any {
|
|
59
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
|
|
60
|
+
return v.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
if (v === "true") return true;
|
|
63
|
+
if (v === "false") return false;
|
|
64
|
+
if (v === "null") return null;
|
|
65
|
+
if (v === "") return "";
|
|
66
|
+
if (/^-?\d+(\.\d+)?$/.test(v)) return Number(v);
|
|
67
|
+
return v;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function parseYaml(yamlStr: string): any {
|
|
71
|
+
const lines = yamlStr.split(/\r?\n/).map(line => {
|
|
72
|
+
const commentIdx = line.indexOf('#');
|
|
73
|
+
const cleanLine = commentIdx !== -1 ? line.slice(0, commentIdx) : line;
|
|
74
|
+
return {
|
|
75
|
+
trimmed: cleanLine.trim(),
|
|
76
|
+
indent: cleanLine.length - cleanLine.trimStart().length
|
|
77
|
+
};
|
|
78
|
+
}).filter(l => l.trimmed !== '');
|
|
79
|
+
|
|
80
|
+
let idx = 0;
|
|
81
|
+
|
|
82
|
+
function parseBlock(baseIndent: number): any {
|
|
83
|
+
let result: any = null;
|
|
84
|
+
let isArray = false;
|
|
85
|
+
|
|
86
|
+
if (idx < lines.length) {
|
|
87
|
+
if (lines[idx].trimmed.startsWith('-')) {
|
|
88
|
+
isArray = true;
|
|
89
|
+
result = [];
|
|
90
|
+
} else {
|
|
91
|
+
result = {};
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
while (idx < lines.length) {
|
|
96
|
+
const line = lines[idx];
|
|
97
|
+
if (line.indent < baseIndent) {
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (isArray) {
|
|
102
|
+
if (!line.trimmed.startsWith('-')) {
|
|
103
|
+
if (result.length > 0 && typeof result[result.length - 1] === 'object') {
|
|
104
|
+
const colonIdx = line.trimmed.indexOf(':');
|
|
105
|
+
if (colonIdx !== -1) {
|
|
106
|
+
const k = line.trimmed.slice(0, colonIdx).trim();
|
|
107
|
+
const rawVal = line.trimmed.slice(colonIdx + 1).trim();
|
|
108
|
+
if (rawVal === '') {
|
|
109
|
+
idx++;
|
|
110
|
+
result[result.length - 1][k] = parseBlock(line.indent + 1);
|
|
111
|
+
continue;
|
|
112
|
+
} else {
|
|
113
|
+
result[result.length - 1][k] = parseValue(rawVal);
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
throw new Error(`Invalid line inside array block: "${line.trimmed}"`);
|
|
117
|
+
}
|
|
118
|
+
} else {
|
|
119
|
+
throw new Error(`Invalid line in array: "${line.trimmed}"`);
|
|
120
|
+
}
|
|
121
|
+
idx++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const rest = line.trimmed.slice(1).trim();
|
|
126
|
+
if (rest === '') {
|
|
127
|
+
idx++;
|
|
128
|
+
const nested = parseBlock(line.indent + 1);
|
|
129
|
+
result.push(nested);
|
|
130
|
+
} else if (rest.includes(':')) {
|
|
131
|
+
const colonIdx = rest.indexOf(':');
|
|
132
|
+
const k = rest.slice(0, colonIdx).trim();
|
|
133
|
+
const rawVal = rest.slice(colonIdx + 1).trim();
|
|
134
|
+
if (rawVal === '') {
|
|
135
|
+
idx++;
|
|
136
|
+
const nestedObj = { [k]: parseBlock(line.indent + 2) };
|
|
137
|
+
result.push(nestedObj);
|
|
138
|
+
} else {
|
|
139
|
+
const item: any = { [k]: parseValue(rawVal) };
|
|
140
|
+
result.push(item);
|
|
141
|
+
idx++;
|
|
142
|
+
while (idx < lines.length && !lines[idx].trimmed.startsWith('-') && lines[idx].indent >= line.indent + 2) {
|
|
143
|
+
const subLine = lines[idx];
|
|
144
|
+
const subColonIdx = subLine.trimmed.indexOf(':');
|
|
145
|
+
if (subColonIdx !== -1) {
|
|
146
|
+
const subK = subLine.trimmed.slice(0, subColonIdx).trim();
|
|
147
|
+
const rawSubVal = subLine.trimmed.slice(subColonIdx + 1).trim();
|
|
148
|
+
if (rawSubVal === '') {
|
|
149
|
+
idx++;
|
|
150
|
+
item[subK] = parseBlock(subLine.indent + 1);
|
|
151
|
+
} else {
|
|
152
|
+
item[subK] = parseValue(rawSubVal);
|
|
153
|
+
idx++;
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
throw new Error(`Invalid sub-line in block mapping: "${subLine.trimmed}"`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} else {
|
|
161
|
+
result.push(parseValue(rest));
|
|
162
|
+
idx++;
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
const colonIdx = line.trimmed.indexOf(':');
|
|
166
|
+
if (colonIdx === -1) {
|
|
167
|
+
throw new Error(`Invalid line: "${line.trimmed}"`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const k = line.trimmed.slice(0, colonIdx).trim();
|
|
171
|
+
const rawVal = line.trimmed.slice(colonIdx + 1).trim();
|
|
172
|
+
|
|
173
|
+
if (rawVal === '') {
|
|
174
|
+
idx++;
|
|
175
|
+
result[k] = parseBlock(line.indent + 1);
|
|
176
|
+
} else {
|
|
177
|
+
result[k] = parseValue(rawVal);
|
|
178
|
+
idx++;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return parseBlock(0);
|
|
187
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared seed-document helpers (round-12, architect ref 8-Round10Planning #5).
|
|
3
|
+
*
|
|
4
|
+
* The deep-interview WRITER and the ultragoal READER of seed lists used to live
|
|
5
|
+
* in different files with different rules — the writer JSON-encoded each value
|
|
6
|
+
* while the reader stripped EVERY double quote, so a criterion like
|
|
7
|
+
* `Display "Done" message` was mangled to `Display \Done\ message` by the time
|
|
8
|
+
* it reached the verification report. Writer and parser now share one module
|
|
9
|
+
* and one encoding, and deep-interview asserts the round-trip at freeze time so
|
|
10
|
+
* any future drift fails loudly instead of corrupting the ledger silently.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Serialize a named YAML list with JSON-encoded scalar items. */
|
|
14
|
+
export function yamlList(name: string, values: string[]): string {
|
|
15
|
+
if (values.length === 0) return `${name}: []`;
|
|
16
|
+
return `${name}:\n${values.map(value => ` - ${JSON.stringify(value)}`).join("\n")}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Parse a named list out of a seed document written by `yamlList` (with a
|
|
20
|
+
* lenient fallback for legacy / hand-edited unquoted items). */
|
|
21
|
+
export function parseSeedList(content: string, name: string): string[] {
|
|
22
|
+
const out: string[] = [];
|
|
23
|
+
let inList = false;
|
|
24
|
+
for (const line of content.split("\n")) {
|
|
25
|
+
const trimmed = line.trim();
|
|
26
|
+
if (trimmed.startsWith(`${name}:`)) {
|
|
27
|
+
inList = true;
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
if (!inList) continue;
|
|
31
|
+
if (trimmed.startsWith("- ")) {
|
|
32
|
+
const rawValue = trimmed.replace(/^-\s*/, "");
|
|
33
|
+
if (rawValue.startsWith('"')) {
|
|
34
|
+
try {
|
|
35
|
+
out.push(JSON.parse(rawValue) as string);
|
|
36
|
+
continue;
|
|
37
|
+
} catch { /* not a clean JSON string — fall through to the lenient path */ }
|
|
38
|
+
}
|
|
39
|
+
// Legacy/hand-written item: strip only a MATCHED outer quote pair, never
|
|
40
|
+
// interior quotes (the old reader's replace(/"/g,"") mangled those).
|
|
41
|
+
out.push(rawValue.replace(/^"(.*)"$/, "$1").replace(/^'(.*)'$/, "$1").trim());
|
|
42
|
+
} else if (trimmed === "" || /^[A-Za-z_][\w-]*:/.test(trimmed)) {
|
|
43
|
+
inList = false; // blank line or next section header ends the list
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return out.filter(v => v.length > 0);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The acceptance-criteria list ultragoal verifies against. */
|
|
50
|
+
export function parseSeedAcceptanceCriteria(content: string): string[] {
|
|
51
|
+
return parseSeedList(content, "acceptance_criteria");
|
|
52
|
+
}
|
package/src/agent/session.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Message } from "./loop";
|
|
2
|
-
import {
|
|
2
|
+
import { getLocalJeoDir } from "./state";
|
|
3
3
|
import * as fs from "node:fs/promises";
|
|
4
4
|
import * as path from "node:path";
|
|
5
5
|
|
|
@@ -9,6 +9,7 @@ export interface SessionHeader {
|
|
|
9
9
|
id: string;
|
|
10
10
|
timestamp: string;
|
|
11
11
|
cwd: string;
|
|
12
|
+
title?: string;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export interface SessionEntry {
|
|
@@ -17,6 +18,13 @@ export interface SessionEntry {
|
|
|
17
18
|
message: Message;
|
|
18
19
|
}
|
|
19
20
|
|
|
21
|
+
export interface CompactionEntry {
|
|
22
|
+
type: "compaction";
|
|
23
|
+
timestamp: string;
|
|
24
|
+
seq: number;
|
|
25
|
+
summary: string;
|
|
26
|
+
replacesThrough: number;
|
|
27
|
+
}
|
|
20
28
|
export interface SessionSummary {
|
|
21
29
|
id: string;
|
|
22
30
|
timestamp: string;
|
|
@@ -24,6 +32,7 @@ export interface SessionSummary {
|
|
|
24
32
|
messageCount: number;
|
|
25
33
|
preview: string;
|
|
26
34
|
mtimeMs?: number;
|
|
35
|
+
title?: string;
|
|
27
36
|
}
|
|
28
37
|
|
|
29
38
|
export const SESSION_VERSION = 1;
|
|
@@ -33,7 +42,7 @@ export function newSessionId(): string {
|
|
|
33
42
|
}
|
|
34
43
|
|
|
35
44
|
export function sessionsDir(cwd = process.cwd()): string {
|
|
36
|
-
return path.join(
|
|
45
|
+
return path.join(getLocalJeoDir(cwd), "sessions");
|
|
37
46
|
}
|
|
38
47
|
|
|
39
48
|
export function sessionPath(id: string, cwd = process.cwd()): string {
|
|
@@ -76,6 +85,41 @@ export async function appendMessage(
|
|
|
76
85
|
await fs.appendFile(file, JSON.stringify(entry) + "\n", "utf8");
|
|
77
86
|
}
|
|
78
87
|
|
|
88
|
+
/** Append a batch of messages with ONE fs append (turn-end persistence: a long
|
|
89
|
+
* turn previously issued one sequential appendFile per intermediate message). */
|
|
90
|
+
export async function appendMessages(
|
|
91
|
+
id: string,
|
|
92
|
+
messages: readonly Message[],
|
|
93
|
+
cwd = process.cwd()
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
if (messages.length === 0) return;
|
|
96
|
+
const file = sessionPath(id, cwd);
|
|
97
|
+
const timestamp = new Date().toISOString();
|
|
98
|
+
const chunk = messages
|
|
99
|
+
.map(message => JSON.stringify({ type: "message", timestamp, message } satisfies SessionEntry))
|
|
100
|
+
.join("\n") + "\n";
|
|
101
|
+
await fs.appendFile(file, chunk, "utf8");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function appendCompaction(
|
|
105
|
+
id: string,
|
|
106
|
+
seq: number,
|
|
107
|
+
summary: string,
|
|
108
|
+
replacesThrough: number,
|
|
109
|
+
cwd = process.cwd()
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const file = sessionPath(id, cwd);
|
|
112
|
+
const entry: CompactionEntry = {
|
|
113
|
+
type: "compaction",
|
|
114
|
+
timestamp: new Date().toISOString(),
|
|
115
|
+
seq,
|
|
116
|
+
summary,
|
|
117
|
+
replacesThrough,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
await fs.appendFile(file, JSON.stringify(entry) + "\n", "utf8");
|
|
121
|
+
}
|
|
122
|
+
|
|
79
123
|
export async function loadSession(
|
|
80
124
|
id: string,
|
|
81
125
|
cwd = process.cwd()
|
|
@@ -93,7 +137,8 @@ export async function loadSession(
|
|
|
93
137
|
|
|
94
138
|
const lines = content.split("\n");
|
|
95
139
|
let header: SessionHeader | undefined;
|
|
96
|
-
const
|
|
140
|
+
const rawMessages: Message[] = [];
|
|
141
|
+
const compactions: CompactionEntry[] = [];
|
|
97
142
|
|
|
98
143
|
for (const line of lines) {
|
|
99
144
|
if (!line.trim()) continue;
|
|
@@ -103,7 +148,9 @@ export async function loadSession(
|
|
|
103
148
|
if (entry.type === "session" && !header) {
|
|
104
149
|
header = entry as SessionHeader;
|
|
105
150
|
} else if (entry.type === "message") {
|
|
106
|
-
|
|
151
|
+
rawMessages.push(entry.message);
|
|
152
|
+
} else if (entry.type === "compaction") {
|
|
153
|
+
compactions.push(entry as CompactionEntry);
|
|
107
154
|
}
|
|
108
155
|
}
|
|
109
156
|
} catch (err) {
|
|
@@ -118,6 +165,23 @@ export async function loadSession(
|
|
|
118
165
|
throw new Error(`Session header missing in session ${id}`);
|
|
119
166
|
}
|
|
120
167
|
|
|
168
|
+
let messages = rawMessages;
|
|
169
|
+
if (compactions.length > 0) {
|
|
170
|
+
const lastComp = compactions[compactions.length - 1];
|
|
171
|
+
const { summary, replacesThrough } = lastComp;
|
|
172
|
+
|
|
173
|
+
const hasSystem = rawMessages.length > 0 && rawMessages[0].role === "system";
|
|
174
|
+
const systemPrompt = hasSystem ? [rawMessages[0]] : [];
|
|
175
|
+
|
|
176
|
+
const summaryMessage: Message = {
|
|
177
|
+
role: "user",
|
|
178
|
+
content: `[Earlier conversation summary]\n${summary}`,
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
const remaining = rawMessages.slice(replacesThrough + 1);
|
|
182
|
+
messages = [...systemPrompt, summaryMessage, ...remaining];
|
|
183
|
+
}
|
|
184
|
+
|
|
121
185
|
return { header, messages };
|
|
122
186
|
}
|
|
123
187
|
|
|
@@ -143,33 +207,76 @@ export async function listSessions(cwd = process.cwd()): Promise<SessionSummary[
|
|
|
143
207
|
const content = await fs.readFile(filePath, "utf8");
|
|
144
208
|
const lines = content.split("\n");
|
|
145
209
|
let header: SessionHeader | undefined;
|
|
146
|
-
let messageCount = 0;
|
|
147
|
-
let firstUserMessageContent: string | undefined;
|
|
148
210
|
|
|
211
|
+
// 1. 헤더만 JSON.parse하여 획득 (가장 첫 valid JSON)
|
|
149
212
|
for (const line of lines) {
|
|
150
213
|
if (!line.trim()) continue;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
214
|
+
if (line.includes('"type":"session"')) {
|
|
215
|
+
try {
|
|
216
|
+
header = JSON.parse(line);
|
|
217
|
+
break;
|
|
218
|
+
} catch {
|
|
219
|
+
// continue
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (!header) {
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// 2. 마지막 compaction 마커 라인을 역순 탐색
|
|
229
|
+
let lastCompaction: CompactionEntry | undefined;
|
|
230
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
231
|
+
const line = lines[i];
|
|
232
|
+
if (line.includes('"type":"compaction"')) {
|
|
233
|
+
try {
|
|
234
|
+
lastCompaction = JSON.parse(line);
|
|
235
|
+
break;
|
|
236
|
+
} catch {
|
|
237
|
+
// continue
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 3. JSON.parse 오버헤드 최소화하며 카운팅 및 프리뷰 추출
|
|
243
|
+
let messageCount = 0;
|
|
244
|
+
let firstUserMessageContent: string | undefined;
|
|
245
|
+
|
|
246
|
+
if (lastCompaction) {
|
|
247
|
+
let msgIndex = 0;
|
|
248
|
+
for (const line of lines) {
|
|
249
|
+
if (!line.trim()) continue;
|
|
250
|
+
if (line.includes('"type":"message"')) {
|
|
251
|
+
if (msgIndex > lastCompaction.replacesThrough) {
|
|
157
252
|
messageCount++;
|
|
158
|
-
if (!firstUserMessageContent && entry.message?.role === "user") {
|
|
159
|
-
firstUserMessageContent = entry.message.content;
|
|
160
|
-
}
|
|
161
253
|
}
|
|
254
|
+
msgIndex++;
|
|
162
255
|
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
256
|
+
}
|
|
257
|
+
messageCount += 1; // summary message
|
|
258
|
+
} else {
|
|
259
|
+
for (const line of lines) {
|
|
260
|
+
if (!line.trim()) continue;
|
|
261
|
+
if (line.includes('"type":"message"')) {
|
|
262
|
+
messageCount++;
|
|
166
263
|
}
|
|
167
|
-
continue;
|
|
168
264
|
}
|
|
169
265
|
}
|
|
170
266
|
|
|
171
|
-
|
|
172
|
-
|
|
267
|
+
// 4. preview를 위한 첫 user message 추출
|
|
268
|
+
for (const line of lines) {
|
|
269
|
+
if (line.includes('"type":"message"') && line.includes('"role":"user"')) {
|
|
270
|
+
try {
|
|
271
|
+
const parsed = JSON.parse(line);
|
|
272
|
+
if (parsed?.message?.content) {
|
|
273
|
+
firstUserMessageContent = parsed.message.content;
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
} catch {
|
|
277
|
+
// continue
|
|
278
|
+
}
|
|
279
|
+
}
|
|
173
280
|
}
|
|
174
281
|
|
|
175
282
|
const preview = firstUserMessageContent ? firstUserMessageContent.slice(0, 60) : "";
|
|
@@ -181,6 +288,7 @@ export async function listSessions(cwd = process.cwd()): Promise<SessionSummary[
|
|
|
181
288
|
messageCount,
|
|
182
289
|
preview,
|
|
183
290
|
mtimeMs: stat.mtimeMs,
|
|
291
|
+
title: header.title,
|
|
184
292
|
});
|
|
185
293
|
} catch {
|
|
186
294
|
// Tolerate malformed files (skip them)
|
|
@@ -196,3 +304,109 @@ export async function latestSessionId(cwd = process.cwd()): Promise<string | und
|
|
|
196
304
|
const list = await listSessions(cwd);
|
|
197
305
|
return list[0]?.id;
|
|
198
306
|
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Rename a session by updating the title in its JSONL header.
|
|
310
|
+
* Throws a clear Error if the session file does not exist.
|
|
311
|
+
*/
|
|
312
|
+
export async function renameSession(id: string, title: string, cwd = process.cwd()): Promise<void> {
|
|
313
|
+
const file = sessionPath(id, cwd);
|
|
314
|
+
let content: string;
|
|
315
|
+
try {
|
|
316
|
+
content = await fs.readFile(file, "utf8");
|
|
317
|
+
} catch (err: any) {
|
|
318
|
+
if (err.code === "ENOENT") {
|
|
319
|
+
throw new Error(`Session ${id} does not exist: ${err.message}`);
|
|
320
|
+
}
|
|
321
|
+
throw err;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const lines = content.split("\n");
|
|
325
|
+
let headerIndex = -1;
|
|
326
|
+
let header: SessionHeader | undefined;
|
|
327
|
+
|
|
328
|
+
for (let i = 0; i < lines.length; i++) {
|
|
329
|
+
if (lines[i].trim()) {
|
|
330
|
+
try {
|
|
331
|
+
const parsed = JSON.parse(lines[i]);
|
|
332
|
+
if (parsed && typeof parsed === "object" && parsed.type === "session") {
|
|
333
|
+
header = parsed as SessionHeader;
|
|
334
|
+
headerIndex = i;
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
} catch {
|
|
338
|
+
// tolerate parsing error or check next line
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (headerIndex === -1 || !header) {
|
|
344
|
+
throw new Error(`Session header missing in session ${id}`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
header.title = title;
|
|
348
|
+
lines[headerIndex] = JSON.stringify(header);
|
|
349
|
+
await fs.writeFile(file, lines.join("\n"), "utf8");
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Delete a session file.
|
|
354
|
+
* Returns false on ENOENT, true on success.
|
|
355
|
+
*/
|
|
356
|
+
export async function deleteSession(id: string, cwd = process.cwd()): Promise<boolean> {
|
|
357
|
+
const file = sessionPath(id, cwd);
|
|
358
|
+
try {
|
|
359
|
+
await fs.unlink(file);
|
|
360
|
+
return true;
|
|
361
|
+
} catch (err: any) {
|
|
362
|
+
if (err.code === "ENOENT") {
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
throw err;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export interface ExportOptions {
|
|
370
|
+
/** Include system messages in the export (default false — they're boilerplate). */
|
|
371
|
+
includeSystem?: boolean;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Render a saved session to Markdown or JSON for handoff, bug reports, and audit
|
|
376
|
+
* trails. Reuses `loadSession` (which tolerates a malformed trailing line).
|
|
377
|
+
*/
|
|
378
|
+
export async function exportSession(
|
|
379
|
+
id: string,
|
|
380
|
+
format: "markdown" | "json" = "markdown",
|
|
381
|
+
cwd = process.cwd(),
|
|
382
|
+
opts: ExportOptions = {},
|
|
383
|
+
): Promise<string> {
|
|
384
|
+
const { header, messages } = await loadSession(id, cwd);
|
|
385
|
+
const picked = opts.includeSystem ? messages : messages.filter(m => m.role !== "system");
|
|
386
|
+
|
|
387
|
+
if (format === "json") {
|
|
388
|
+
return JSON.stringify(
|
|
389
|
+
{ id: header.id, timestamp: header.timestamp, cwd: header.cwd, messageCount: picked.length, messages: picked },
|
|
390
|
+
null,
|
|
391
|
+
2,
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const lines: string[] = [
|
|
396
|
+
`# jeo session ${header.id}`,
|
|
397
|
+
"",
|
|
398
|
+
`- Started: ${header.timestamp}`,
|
|
399
|
+
`- Workspace: ${header.cwd}`,
|
|
400
|
+
`- Messages: ${picked.length}`,
|
|
401
|
+
"",
|
|
402
|
+
];
|
|
403
|
+
for (const m of picked) {
|
|
404
|
+
const role = m.role.charAt(0).toUpperCase() + m.role.slice(1);
|
|
405
|
+
// Fence longer than the longest backtick run in the body (CommonMark) so message
|
|
406
|
+
// content containing ``` doesn't prematurely close the code fence.
|
|
407
|
+
const longest = (m.content.match(/`+/g) ?? []).reduce((mx, r) => Math.max(mx, r.length), 0);
|
|
408
|
+
const fence = "`".repeat(Math.max(3, longest + 1));
|
|
409
|
+
lines.push(`## ${role}`, "", fence, m.content, fence, "");
|
|
410
|
+
}
|
|
411
|
+
return lines.join("\n");
|
|
412
|
+
}
|