@akanjs/cli 3.0.0-beta.14 → 3.0.0-beta.16
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/.build-stamp +1 -1
- package/CodeAgent-zgccwajm.js +9 -0
- package/CodeAgentRpcHost-qvwfv0n9.js +7 -0
- package/CodeTui-993tx7gf.js +1103 -0
- package/README.md +3 -0
- package/{agent.command-bhwsc2aj.js → agent.command-n664dtcv.js} +6 -6
- package/akanCode.js +100 -0
- package/{application.command-436y543n.js → application.command-t5542jte.js} +6 -5
- package/{applicationBuildRunner-4f1wrba0.js → applicationBuildRunner-x4gbfn7y.js} +2 -2
- package/{applicationReleasePackager-jkjmy5ee.js → applicationReleasePackager-z61ep017.js} +1 -1
- package/buildBatch.proc.js +2 -2
- package/{capacitorApp-hqvsrec4.js → capacitorApp-shv3ajh5.js} +2 -2
- package/{cloud.command-3xn0xpz8.js → cloud.command-4x35k1z6.js} +9 -8
- package/cloud.script-3k8nbbpn.js +20 -0
- package/code.command-mrfb7a1g.js +147 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/{context.command-hxh96cc3.js → context.command-pqsm5jrk.js} +14 -13
- package/context.runner-bys6ykcp.js +22 -0
- package/{devTui-4ted58a6.js → devTui-gy6mendg.js} +114 -8
- package/{devTuiView-1dj2yq7t.js → devTuiView-y1548wy6.js} +1 -1
- package/{guideline.command-cyw8c0m0.js → guideline.command-rpc2emw8.js} +6 -6
- package/guidelines/agentRule/agentRule.instruction.md +22 -2
- package/guidelines/conventions/conventions.instruction.md +3 -1
- package/incrementalBuilder.proc.js +1 -1
- package/{index-t8gvwcrx.js → index-018dszb3.js} +13 -4
- package/{index-8pkvrcv1.js → index-2pm1j01n.js} +3 -3
- package/{index-7f4dsf9v.js → index-3cymrzsb.js} +1 -1
- package/{index-1nwxbr2y.js → index-3mg4h788.js} +2 -2
- package/{index-em4swemt.js → index-4cvpzkq6.js} +3 -3
- package/index-4n4821gp.js +69 -0
- package/index-5405kc0x.js +2434 -0
- package/{index-tt6kzwj5.js → index-57p9p179.js} +3 -3
- package/{index-b3ae16mg.js → index-6na0sd2b.js} +1 -1
- package/index-7md2g9vw.js +6 -0
- package/index-8k16atqy.js +82 -0
- package/{index-fad0hhty.js → index-g9zghs4h.js} +22 -80
- package/index-jgrnpt6y.js +121 -0
- package/{index-31hmq4ys.js → index-kvzjda3v.js} +8 -8
- package/{index-sg1bgdhe.js → index-ns128c4q.js} +27 -105
- package/{index-5nyznra1.js → index-p4p9t45x.js} +3 -0
- package/{index-sh88vtye.js → index-qh7brn20.js} +1 -1
- package/{index-jwjaf9fy.js → index-qhtj9y6j.js} +44 -44
- package/{index-ys7dvdb3.js → index-sn7dnzq4.js} +1 -1
- package/index-sxg87a8f.js +727 -0
- package/{index-r73ha1rf.js → index-te0y1c0m.js} +4 -4
- package/{index-h7yw2z9n.js → index-twq9p2sp.js} +8 -8
- package/{index-15r9a10e.js → index-v17gj504.js} +2 -2
- package/{index-1mrmve4s.js → index-vpx68e4x.js} +2 -2
- package/index-wfwknnje.js +124 -0
- package/{index-9fbk3dbh.js → index-xjc7gg65.js} +4 -4
- package/{index-8tcj5wk8.js → index-zg0v1zte.js} +5 -5
- package/{index-vrwdx0zx.js → index-zn8qgeqx.js} +1 -1
- package/index.js +21 -20
- package/{library.command-1f9h2ex6.js → library.command-m8p2r1dh.js} +3 -3
- package/{localRegistry.command-4cbgz63s.js → localRegistry.command-tt00nwwg.js} +8 -7
- package/{module.command-ew0484va.js → module.command-mfzvqt0n.js} +5 -5
- package/{package.command-y2rh4np7.js → package.command-8sy6jjwy.js} +3 -3
- package/package.json +12 -2
- package/{page.command-e5h29fyt.js → page.command-taenb02b.js} +3 -3
- package/{primitive.command-50ecfzg4.js → primitive.command-7kpncmkk.js} +6 -6
- package/prompter-9mn3kp36.js +8 -0
- package/{quality.command-a2jytczs.js → quality.command-1ar94hrw.js} +2 -2
- package/{repair.command-c2amvt74.js → repair.command-n08t6j8p.js} +5 -5
- package/{scalar.command-bptz6aj6.js → scalar.command-mq8qfp4t.js} +4 -4
- package/skills/akan-debug/SKILL.md +42 -0
- package/skills/akan-deps/SKILL.md +50 -0
- package/skills/akan-err/SKILL.md +39 -0
- package/skills/akan-module/SKILL.md +45 -0
- package/skills/akan-scaffold/SKILL.md +76 -0
- package/skills/akan-scalar/SKILL.md +52 -0
- package/skills/akan-service/SKILL.md +34 -0
- package/skills/akan-store/SKILL.md +63 -0
- package/skills/akan-ui/SKILL.md +63 -0
- package/skills/akan-validate/SKILL.md +44 -0
- package/{subspace.command-kmemx1vm.js → subspace.command-x9897gj4.js} +4 -4
- package/{tunnel.command-67q8vsg5.js → tunnel.command-ht2es78c.js} +2 -2
- package/{workflow.command-2ze5y2xp.js → workflow.command-tvz4nm9n.js} +9 -9
- package/{workspace.command-2n385jkt.js → workspace.command-ayqa1rgt.js} +22 -20
- package/cloud.script-0a76c2g8.js +0 -19
|
@@ -0,0 +1,2434 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
SubagentPool2
|
|
4
|
+
} from "./index-jgrnpt6y.js";
|
|
5
|
+
import {
|
|
6
|
+
stripAnsi
|
|
7
|
+
} from "./index-7md2g9vw.js";
|
|
8
|
+
import {
|
|
9
|
+
akanCodeModel2,
|
|
10
|
+
akanCodeModelSupportsImages2,
|
|
11
|
+
akanCodeModelWarnings2
|
|
12
|
+
} from "./index-wfwknnje.js";
|
|
13
|
+
|
|
14
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgent.ts
|
|
15
|
+
import {
|
|
16
|
+
createAgentSession,
|
|
17
|
+
DEFAULT_COMPACTION_SETTINGS,
|
|
18
|
+
DefaultResourceLoader
|
|
19
|
+
} from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import {
|
|
21
|
+
codeAgentClip as codeAgentClip5,
|
|
22
|
+
codeAgentLabelChars as codeAgentLabelChars2,
|
|
23
|
+
codeAgentRenderAnswer,
|
|
24
|
+
codeAgentSessionName as codeAgentSessionName2
|
|
25
|
+
} from "akanjs/common";
|
|
26
|
+
|
|
27
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/DevLogFeedback.ts
|
|
28
|
+
import { existsSync, statSync } from "fs";
|
|
29
|
+
import path from "path";
|
|
30
|
+
|
|
31
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanEditScope.ts
|
|
32
|
+
var empty = { paths: [], apps: [], libs: [], needsSync: false, touchesTsx: false };
|
|
33
|
+
|
|
34
|
+
class AkanEditScope2 {
|
|
35
|
+
static async since(cwd, baseline) {
|
|
36
|
+
const current = await AkanEditScope2.#status(cwd);
|
|
37
|
+
const changed = [...current].filter((line) => !baseline.has(line));
|
|
38
|
+
if (!changed.length)
|
|
39
|
+
return empty;
|
|
40
|
+
const entries = changed.map((line) => ({ code: line.slice(0, 2), file: line.slice(3).split(" -> ").at(-1) ?? "" }));
|
|
41
|
+
const paths = entries.map((entry) => entry.file).filter(Boolean);
|
|
42
|
+
return {
|
|
43
|
+
paths,
|
|
44
|
+
apps: AkanEditScope2.#targets(paths, "apps"),
|
|
45
|
+
libs: AkanEditScope2.#targets(paths, "libs"),
|
|
46
|
+
needsSync: entries.some((entry) => /[ARD?]/.test(entry.code) && /^(apps|libs)\//.test(entry.file)),
|
|
47
|
+
touchesTsx: paths.some((file) => file.endsWith(".tsx"))
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
static async baseline(cwd) {
|
|
51
|
+
return await AkanEditScope2.#status(cwd);
|
|
52
|
+
}
|
|
53
|
+
static async#status(cwd) {
|
|
54
|
+
const proc = Bun.spawn(["git", "status", "--porcelain=v1", "--untracked-files=all"], {
|
|
55
|
+
cwd,
|
|
56
|
+
stdout: "pipe",
|
|
57
|
+
stderr: "ignore"
|
|
58
|
+
});
|
|
59
|
+
const text = await new Response(proc.stdout).text();
|
|
60
|
+
await proc.exited;
|
|
61
|
+
return new Set(text.split(`
|
|
62
|
+
`).filter((line) => line.trim().length > 3));
|
|
63
|
+
}
|
|
64
|
+
static #targets(paths, root) {
|
|
65
|
+
const names = paths.filter((file) => file.startsWith(`${root}/`)).map((file) => file.split("/")[1] ?? "").filter(Boolean);
|
|
66
|
+
return [...new Set(names)];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/DevLogFeedback.ts
|
|
71
|
+
var errorPattern = /\bERROR\b|\bFATAL\b|\[stderr\]|error:|Error:|\u2716|Unhandled|Cannot find|is not a function/;
|
|
72
|
+
var noisePattern = /DEBUG|\bWARN\b|MCP catalogue|rate limit/;
|
|
73
|
+
|
|
74
|
+
class DevLogFeedback2 {
|
|
75
|
+
key = "dev-server";
|
|
76
|
+
#options;
|
|
77
|
+
#marks = new Map;
|
|
78
|
+
#baseline = new Set;
|
|
79
|
+
constructor(options) {
|
|
80
|
+
this.#options = { graceMs: 1500, maxLines: 30, ...options };
|
|
81
|
+
}
|
|
82
|
+
async begin() {
|
|
83
|
+
for (const app of this.#options.apps)
|
|
84
|
+
this.#marks.set(app, DevLogFeedback2.#sizeOf(this.#pathOf(app)));
|
|
85
|
+
this.#baseline = await AkanEditScope2.baseline(this.#options.cwd);
|
|
86
|
+
}
|
|
87
|
+
async observe() {
|
|
88
|
+
const running = this.#options.apps.filter((app) => existsSync(this.#pathOf(app)));
|
|
89
|
+
if (!running.length)
|
|
90
|
+
return;
|
|
91
|
+
if (!(await AkanEditScope2.since(this.#options.cwd, this.#baseline)).paths.length)
|
|
92
|
+
return;
|
|
93
|
+
await Bun.sleep(this.#options.graceMs);
|
|
94
|
+
const reports = [];
|
|
95
|
+
for (const app of running) {
|
|
96
|
+
const lines = await this.#linesSince(app);
|
|
97
|
+
if (lines.length)
|
|
98
|
+
reports.push(`**${app}** dev server:
|
|
99
|
+
\`\`\`
|
|
100
|
+
${lines.join(`
|
|
101
|
+
`)}
|
|
102
|
+
\`\`\``);
|
|
103
|
+
}
|
|
104
|
+
if (!reports.length)
|
|
105
|
+
return;
|
|
106
|
+
return [
|
|
107
|
+
"The dev server logged errors after your edits:",
|
|
108
|
+
...reports,
|
|
109
|
+
`Full log: ${running.map((app) => path.relative(this.#options.workspaceRoot, this.#pathOf(app))).join(", ")}`
|
|
110
|
+
].join(`
|
|
111
|
+
|
|
112
|
+
`);
|
|
113
|
+
}
|
|
114
|
+
async#linesSince(app) {
|
|
115
|
+
const file = this.#pathOf(app);
|
|
116
|
+
const from = this.#marks.get(app) ?? 0;
|
|
117
|
+
const size = DevLogFeedback2.#sizeOf(file);
|
|
118
|
+
if (size <= from)
|
|
119
|
+
return [];
|
|
120
|
+
const text = await Bun.file(file).slice(from, size).text();
|
|
121
|
+
this.#marks.set(app, size);
|
|
122
|
+
return text.split(`
|
|
123
|
+
`).filter((line) => errorPattern.test(line) && !noisePattern.test(line)).slice(-this.#options.maxLines);
|
|
124
|
+
}
|
|
125
|
+
#pathOf(app) {
|
|
126
|
+
return path.join(this.#options.workspaceRoot, "local", "apps", app, "runtime", "dev.log");
|
|
127
|
+
}
|
|
128
|
+
static async previewUrl(workspaceRoot, app) {
|
|
129
|
+
const file = path.join(workspaceRoot, "local", "apps", app, "runtime", "dev.log");
|
|
130
|
+
if (!existsSync(file))
|
|
131
|
+
return;
|
|
132
|
+
const matches = [...(await Bun.file(file).text()).matchAll(/ready \(pid=\d+\) \u2014 (https?:\/\/\S+)/g)];
|
|
133
|
+
return matches.at(-1)?.[1];
|
|
134
|
+
}
|
|
135
|
+
static #sizeOf(file) {
|
|
136
|
+
return existsSync(file) ? statSync(file).size : 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/PreviewChrome.ts
|
|
141
|
+
import { existsSync as existsSync2 } from "fs";
|
|
142
|
+
import { homedir } from "os";
|
|
143
|
+
import path2 from "path";
|
|
144
|
+
|
|
145
|
+
class PreviewChrome2 {
|
|
146
|
+
static pathEnvKey = "BUN_CHROME_PATH";
|
|
147
|
+
static backendEnvKey = "AKAN_CODE_WEBVIEW_BACKEND";
|
|
148
|
+
static executables = [
|
|
149
|
+
"chromium",
|
|
150
|
+
"chromium-browser",
|
|
151
|
+
"google-chrome-stable",
|
|
152
|
+
"google-chrome",
|
|
153
|
+
"brave-browser",
|
|
154
|
+
"microsoft-edge",
|
|
155
|
+
"chrome"
|
|
156
|
+
];
|
|
157
|
+
static appBundles = [
|
|
158
|
+
"Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
159
|
+
"Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
160
|
+
"Chromium.app/Contents/MacOS/Chromium",
|
|
161
|
+
"Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
162
|
+
"Microsoft Edge.app/Contents/MacOS/Microsoft Edge"
|
|
163
|
+
];
|
|
164
|
+
static get backend() {
|
|
165
|
+
const declared = process.env[PreviewChrome2.backendEnvKey];
|
|
166
|
+
if (declared === "webkit" || declared === "chrome")
|
|
167
|
+
return declared;
|
|
168
|
+
return process.platform === "darwin" ? "webkit" : "chrome";
|
|
169
|
+
}
|
|
170
|
+
static executable() {
|
|
171
|
+
const declared = process.env[PreviewChrome2.pathEnvKey];
|
|
172
|
+
if (declared)
|
|
173
|
+
return declared;
|
|
174
|
+
for (const name of PreviewChrome2.executables) {
|
|
175
|
+
const found = Bun.which(name);
|
|
176
|
+
if (found)
|
|
177
|
+
return found;
|
|
178
|
+
}
|
|
179
|
+
if (process.platform !== "darwin")
|
|
180
|
+
return;
|
|
181
|
+
for (const root of ["/Applications", path2.join(homedir(), "Applications")])
|
|
182
|
+
for (const bundle of PreviewChrome2.appBundles) {
|
|
183
|
+
const candidate = path2.join(root, bundle);
|
|
184
|
+
if (existsSync2(candidate))
|
|
185
|
+
return candidate;
|
|
186
|
+
}
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
static get available() {
|
|
190
|
+
return PreviewChrome2.backend === "webkit" ? process.platform === "darwin" : !!PreviewChrome2.executable();
|
|
191
|
+
}
|
|
192
|
+
static backendOption() {
|
|
193
|
+
if (PreviewChrome2.backend === "webkit")
|
|
194
|
+
return "webkit";
|
|
195
|
+
const argv = ["--disable-dev-shm-usage"];
|
|
196
|
+
if (process.getuid?.() === 0)
|
|
197
|
+
argv.push("--no-sandbox");
|
|
198
|
+
return { type: "chrome", argv };
|
|
199
|
+
}
|
|
200
|
+
static dockerRuns = [
|
|
201
|
+
"apt-get update && apt-get install -y --no-install-recommends chromium fonts-liberation fonts-noto-color-emoji fonts-noto-cjk && rm -rf /var/lib/apt/lists/*"
|
|
202
|
+
];
|
|
203
|
+
static unavailableReason() {
|
|
204
|
+
if (PreviewChrome2.available)
|
|
205
|
+
return;
|
|
206
|
+
if (PreviewChrome2.backend === "webkit")
|
|
207
|
+
return `The webkit WebView backend exists only on macOS. Set ${PreviewChrome2.backendEnvKey}=chrome and install a browser.`;
|
|
208
|
+
return [
|
|
209
|
+
"The page preview needs a browser and this machine has none on PATH.",
|
|
210
|
+
`Install one (${PreviewChrome2.dockerRuns[0]}) or point ${PreviewChrome2.pathEnvKey} at an existing Chrome.`
|
|
211
|
+
].join(" ");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/PreviewView.ts
|
|
216
|
+
class PreviewView2 {
|
|
217
|
+
static get available() {
|
|
218
|
+
return typeof Bun.WebView === "function" && PreviewChrome2.available;
|
|
219
|
+
}
|
|
220
|
+
static unavailableReason() {
|
|
221
|
+
if (typeof Bun.WebView !== "function")
|
|
222
|
+
return "This Bun build has no WebView, so the page preview is off.";
|
|
223
|
+
return PreviewChrome2.unavailableReason();
|
|
224
|
+
}
|
|
225
|
+
static async probe(url, { screenshot = false, width = 1280, height = 800 } = {}) {
|
|
226
|
+
const logs = [];
|
|
227
|
+
const view = PreviewView2.#open({ width, height, onConsole: (line) => logs.push(line) });
|
|
228
|
+
try {
|
|
229
|
+
await view.navigate(url);
|
|
230
|
+
const result = {
|
|
231
|
+
url,
|
|
232
|
+
title: view.title,
|
|
233
|
+
consoleErrors: logs.filter((line) => line.startsWith("error") || line.startsWith("warn")),
|
|
234
|
+
pageErrors: PreviewView2.#strings(await view.evaluate(pageErrorsExpression)),
|
|
235
|
+
domFindings: PreviewView2.#strings(await view.evaluate(domFindingsExpression))
|
|
236
|
+
};
|
|
237
|
+
if (screenshot)
|
|
238
|
+
result.screenshot = await view.screenshot({ encoding: "base64" });
|
|
239
|
+
return result;
|
|
240
|
+
} finally {
|
|
241
|
+
await view.close();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
static #open({ width, height, onConsole }) {
|
|
245
|
+
const Ctor = Bun.WebView;
|
|
246
|
+
return new Ctor({
|
|
247
|
+
width,
|
|
248
|
+
height,
|
|
249
|
+
dataStore: "ephemeral",
|
|
250
|
+
backend: PreviewChrome2.backendOption(),
|
|
251
|
+
console: (type, ...args) => onConsole(`${type} ${args.map(String).join(" ")}`)
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
static #strings(value) {
|
|
255
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string") : [];
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
var pageErrorsExpression = `(() => {
|
|
259
|
+
const found = [];
|
|
260
|
+
const root = document.getElementById("__next") || document.body;
|
|
261
|
+
const rendered = root ? Array.from(root.children).filter((el) => !["SCRIPT", "STYLE", "TEMPLATE", "LINK", "NOSCRIPT"].includes(el.tagName)) : [];
|
|
262
|
+
if (!rendered.length && !(root && root.innerText && root.innerText.trim())) found.push("The page body rendered empty.");
|
|
263
|
+
const overlay = document.querySelector("[data-nextjs-dialog], .error-overlay, #akan-error");
|
|
264
|
+
if (overlay) found.push("An error overlay is on the page: " + overlay.textContent.slice(0, 400));
|
|
265
|
+
return found;
|
|
266
|
+
})()`;
|
|
267
|
+
var domFindingsExpression = `(() => {
|
|
268
|
+
const found = [];
|
|
269
|
+
const all = Array.from(document.querySelectorAll("body *"));
|
|
270
|
+
const overflowing = all.filter((el) => el.scrollWidth > document.documentElement.clientWidth + 8);
|
|
271
|
+
if (overflowing.length) found.push(overflowing.length + " element(s) overflow the viewport horizontally, first: " + (overflowing[0].tagName + "." + overflowing[0].className).slice(0, 120));
|
|
272
|
+
const invisibleText = all.filter((el) => {
|
|
273
|
+
if (!el.textContent || !el.textContent.trim()) return false;
|
|
274
|
+
const style = getComputedStyle(el);
|
|
275
|
+
return style.color === style.backgroundColor && style.backgroundColor !== "rgba(0, 0, 0, 0)";
|
|
276
|
+
});
|
|
277
|
+
if (invisibleText.length) found.push(invisibleText.length + " element(s) render text in the same colour as their background.");
|
|
278
|
+
const unresolved = all.filter((el) => /(^|\\s)(bg|text|border)-(base-100|base-content|primary-content|error)(\\s|$)/.test(el.className || ""));
|
|
279
|
+
if (unresolved.length) found.push(unresolved.length + " element(s) use a dropped daisyUI colour slot that renders no CSS.");
|
|
280
|
+
return found;
|
|
281
|
+
})()`;
|
|
282
|
+
|
|
283
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/PreviewFeedback.ts
|
|
284
|
+
class PreviewFeedback2 {
|
|
285
|
+
key = "preview";
|
|
286
|
+
#options;
|
|
287
|
+
#baseline = new Set;
|
|
288
|
+
constructor(options) {
|
|
289
|
+
this.#options = options;
|
|
290
|
+
}
|
|
291
|
+
async begin() {
|
|
292
|
+
this.#baseline = await AkanEditScope2.baseline(this.#options.cwd);
|
|
293
|
+
}
|
|
294
|
+
async observe() {
|
|
295
|
+
if (!this.#options.previewUrl || !PreviewView2.available)
|
|
296
|
+
return;
|
|
297
|
+
const scope = await AkanEditScope2.since(this.#options.cwd, this.#baseline);
|
|
298
|
+
if (!scope.touchesTsx)
|
|
299
|
+
return;
|
|
300
|
+
const findings = [];
|
|
301
|
+
const images = [];
|
|
302
|
+
for (const route of this.#options.routes ?? ["/en/"]) {
|
|
303
|
+
const url = new URL(route, this.#options.previewUrl).toString();
|
|
304
|
+
const probe = await PreviewView2.probe(url, { screenshot: !!this.#options.canSeeImages });
|
|
305
|
+
const problems = [...probe.pageErrors, ...probe.consoleErrors, ...probe.domFindings];
|
|
306
|
+
if (!problems.length)
|
|
307
|
+
continue;
|
|
308
|
+
findings.push(`**${url}**
|
|
309
|
+
${problems.map((line) => `- ${line}`).join(`
|
|
310
|
+
`)}`);
|
|
311
|
+
if (probe.screenshot)
|
|
312
|
+
images.push({ data: probe.screenshot, mimeType: "image/png" });
|
|
313
|
+
}
|
|
314
|
+
if (!findings.length)
|
|
315
|
+
return;
|
|
316
|
+
return { text: ["The rendered page has problems after your UI edits:", ...findings].join(`
|
|
317
|
+
|
|
318
|
+
`), images };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/TurnFeedback.ts
|
|
323
|
+
class TurnFeedback2 {
|
|
324
|
+
#sources;
|
|
325
|
+
#options;
|
|
326
|
+
#spent = new Map;
|
|
327
|
+
constructor(sources, options) {
|
|
328
|
+
this.#sources = sources;
|
|
329
|
+
this.#options = options;
|
|
330
|
+
}
|
|
331
|
+
extension() {
|
|
332
|
+
return {
|
|
333
|
+
name: "akan-turn-feedback",
|
|
334
|
+
factory: (pi) => {
|
|
335
|
+
pi.on("agent_start", async () => {
|
|
336
|
+
await Promise.all(this.#sources.map((source) => TurnFeedback2.#quietly(() => source.begin())));
|
|
337
|
+
});
|
|
338
|
+
pi.on("agent_end", async () => {
|
|
339
|
+
const message = await this.collect();
|
|
340
|
+
if (message)
|
|
341
|
+
pi.sendUserMessage(message, { deliverAs: "followUp" });
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
async collect() {
|
|
347
|
+
const texts = [];
|
|
348
|
+
const images = [];
|
|
349
|
+
const reopened = [];
|
|
350
|
+
for (const source of this.#sources) {
|
|
351
|
+
const finding = await TurnFeedback2.#quietly(() => source.observe());
|
|
352
|
+
if (!finding) {
|
|
353
|
+
this.#spent.delete(source.key);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
const spent = this.#spent.get(source.key) ?? 0;
|
|
357
|
+
if (spent >= this.#options.budget) {
|
|
358
|
+
this.#options.onNotice?.(`${source.key}: still failing after ${spent} attempts \u2014 handing back to you.`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
this.#spent.set(source.key, spent + 1);
|
|
362
|
+
reopened.push(source.key);
|
|
363
|
+
if (typeof finding === "string")
|
|
364
|
+
texts.push(finding);
|
|
365
|
+
else {
|
|
366
|
+
texts.push(finding.text);
|
|
367
|
+
images.push(...finding.images ?? []);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (!texts.length)
|
|
371
|
+
return;
|
|
372
|
+
this.#options.onNotice?.(`${reopened.join(" \xB7 ")} reopened the turn \u2014 esc stops it`);
|
|
373
|
+
const text = [
|
|
374
|
+
"The previous turn left problems you cannot see from the transcript. Fix them, then stop.",
|
|
375
|
+
...texts
|
|
376
|
+
].join(`
|
|
377
|
+
|
|
378
|
+
`);
|
|
379
|
+
return images.length ? [{ type: "text", text }, ...images.map((image) => ({ type: "image", ...image }))] : text;
|
|
380
|
+
}
|
|
381
|
+
static async#quietly(fn) {
|
|
382
|
+
try {
|
|
383
|
+
return await fn();
|
|
384
|
+
} catch {
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanCli.ts
|
|
391
|
+
import { existsSync as existsSync3 } from "fs";
|
|
392
|
+
import path3 from "path";
|
|
393
|
+
class AkanCli2 {
|
|
394
|
+
#cwd;
|
|
395
|
+
#entry;
|
|
396
|
+
constructor(cwd) {
|
|
397
|
+
this.#cwd = cwd;
|
|
398
|
+
this.#entry = AkanCli2.#resolveEntry();
|
|
399
|
+
}
|
|
400
|
+
async run(args, { timeoutMs = 300000 } = {}) {
|
|
401
|
+
const proc = Bun.spawn([process.execPath, this.#entry, ...args], {
|
|
402
|
+
cwd: this.#cwd,
|
|
403
|
+
stdout: "pipe",
|
|
404
|
+
stderr: "pipe",
|
|
405
|
+
env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" }
|
|
406
|
+
});
|
|
407
|
+
const timer = setTimeout(() => proc.kill(), timeoutMs);
|
|
408
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
409
|
+
new Response(proc.stdout).text(),
|
|
410
|
+
new Response(proc.stderr).text(),
|
|
411
|
+
proc.exited
|
|
412
|
+
]);
|
|
413
|
+
clearTimeout(timer);
|
|
414
|
+
const output = stripAnsi(`${stdout}${stderr}`).trim();
|
|
415
|
+
return { command: `akan ${args.join(" ")}`, ok: exitCode === 0, exitCode, output };
|
|
416
|
+
}
|
|
417
|
+
static #resolveEntry() {
|
|
418
|
+
const candidates = [
|
|
419
|
+
path3.join(import.meta.dir, "index.js"),
|
|
420
|
+
path3.join(import.meta.dir, "..", "..", "..", "cli", "index.ts")
|
|
421
|
+
];
|
|
422
|
+
return candidates.find((candidate) => existsSync3(candidate)) ?? Bun.main;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanVerifier.ts
|
|
427
|
+
class AkanVerifier2 {
|
|
428
|
+
#cli;
|
|
429
|
+
constructor(cwd) {
|
|
430
|
+
this.#cli = new AkanCli2(cwd);
|
|
431
|
+
}
|
|
432
|
+
async verify(scope) {
|
|
433
|
+
const steps = [];
|
|
434
|
+
const targets = [...scope.apps, ...scope.libs];
|
|
435
|
+
if (scope.needsSync)
|
|
436
|
+
for (const target of targets)
|
|
437
|
+
steps.push(await this.#cli.run(["sync", target]));
|
|
438
|
+
for (const target of targets)
|
|
439
|
+
steps.push(await this.#cli.run(["lint", target]));
|
|
440
|
+
for (const app of scope.apps)
|
|
441
|
+
steps.push(await this.#cli.run(["typecheck", app]));
|
|
442
|
+
if (scope.touchesTsx)
|
|
443
|
+
steps.push(await this.#cli.run(["quality", "ssr"]));
|
|
444
|
+
return { ok: steps.every((step) => step.ok), steps, scope };
|
|
445
|
+
}
|
|
446
|
+
static summarize(report, { tailLines = 40 } = {}) {
|
|
447
|
+
if (!report.steps.length)
|
|
448
|
+
return "No akan target was touched, so there was nothing to verify.";
|
|
449
|
+
if (report.ok)
|
|
450
|
+
return `Verification passed: ${report.steps.map((step) => step.command).join(", ")}.`;
|
|
451
|
+
const failed = report.steps.filter((step) => !step.ok);
|
|
452
|
+
const passed = report.steps.filter((step) => step.ok).map((step) => step.command);
|
|
453
|
+
const blocks = failed.map((step) => `### ${step.command} (exit ${step.exitCode})
|
|
454
|
+
${tail(step.output, tailLines)}`);
|
|
455
|
+
return [
|
|
456
|
+
`Verification failed: ${failed.map((step) => step.command).join(", ")}.`,
|
|
457
|
+
passed.length ? `Passed: ${passed.join(", ")}.` : "",
|
|
458
|
+
...blocks
|
|
459
|
+
].filter(Boolean).join(`
|
|
460
|
+
|
|
461
|
+
`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
var tail = (text, lines) => {
|
|
465
|
+
const all = text.split(`
|
|
466
|
+
`);
|
|
467
|
+
return all.length <= lines ? text : `\u2026
|
|
468
|
+
${all.slice(-lines).join(`
|
|
469
|
+
`)}`;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/VerifyFeedback.ts
|
|
473
|
+
class VerifyFeedback2 {
|
|
474
|
+
key = "verify";
|
|
475
|
+
#options;
|
|
476
|
+
#verifier;
|
|
477
|
+
#baseline = new Set;
|
|
478
|
+
constructor(options) {
|
|
479
|
+
this.#options = options;
|
|
480
|
+
this.#verifier = new AkanVerifier2(options.cwd);
|
|
481
|
+
}
|
|
482
|
+
async begin() {
|
|
483
|
+
this.#baseline = await AkanEditScope2.baseline(this.#options.cwd);
|
|
484
|
+
}
|
|
485
|
+
async observe() {
|
|
486
|
+
const scope = await AkanEditScope2.since(this.#options.cwd, this.#baseline);
|
|
487
|
+
if (!scope.apps.length && !scope.libs.length)
|
|
488
|
+
return;
|
|
489
|
+
if (this.#options.readOnly)
|
|
490
|
+
return VerifyFeedback2.#readOnlyNotice(scope.needsSync, [...scope.apps, ...scope.libs]);
|
|
491
|
+
const report = await this.#verifier.verify(scope);
|
|
492
|
+
if (report.ok)
|
|
493
|
+
return;
|
|
494
|
+
return AkanVerifier2.summarize(report);
|
|
495
|
+
}
|
|
496
|
+
static #readOnlyNotice(needsSync, targets) {
|
|
497
|
+
if (!needsSync)
|
|
498
|
+
return;
|
|
499
|
+
return `Files were added or removed under ${targets.join(", ")}, so the generated barrels are stale. This profile does not write, so run \`akan sync ${targets.join(" ")}\` yourself before trusting a typecheck.`;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanToolPack.ts
|
|
504
|
+
import { Type } from "typebox";
|
|
505
|
+
class AkanToolPack2 {
|
|
506
|
+
#options;
|
|
507
|
+
constructor(options) {
|
|
508
|
+
this.#options = options;
|
|
509
|
+
}
|
|
510
|
+
extension() {
|
|
511
|
+
return { name: "akan-tools", factory: async (pi) => await this.#register(pi) };
|
|
512
|
+
}
|
|
513
|
+
async names() {
|
|
514
|
+
const { ContextRunner } = await import("./context.runner-bys6ykcp.js");
|
|
515
|
+
return [
|
|
516
|
+
...new ContextRunner().listMcpTools(this.#options.mode).map((tool) => tool.name),
|
|
517
|
+
AkanToolPack2.verifyToolName
|
|
518
|
+
];
|
|
519
|
+
}
|
|
520
|
+
static verifyToolName = "akan_verify";
|
|
521
|
+
async#register(pi) {
|
|
522
|
+
const { ContextRunner } = await import("./context.runner-bys6ykcp.js");
|
|
523
|
+
const runner = new ContextRunner;
|
|
524
|
+
const guidelineNames = await AkanToolPack2.#guidelineNames();
|
|
525
|
+
for (const tool of runner.listMcpTools(this.#options.mode, { guidelineNames })) {
|
|
526
|
+
const description = tool.description ?? tool.name;
|
|
527
|
+
pi.registerTool({
|
|
528
|
+
name: tool.name,
|
|
529
|
+
label: tool.name,
|
|
530
|
+
description,
|
|
531
|
+
promptSnippet: `${tool.name}: ${description.split(".")[0] ?? description}`,
|
|
532
|
+
parameters: Type.Unsafe(tool.inputSchema),
|
|
533
|
+
execute: async (_id, params) => {
|
|
534
|
+
const result = await runner.callMcpTool(this.#options.workspace, tool.name, params ?? {}, {
|
|
535
|
+
mode: this.#options.mode
|
|
536
|
+
});
|
|
537
|
+
return { content: [{ type: "text", text: AkanToolPack2.#render(result) }], details: undefined };
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
this.#registerVerify(pi);
|
|
542
|
+
}
|
|
543
|
+
#registerVerify(pi) {
|
|
544
|
+
const verifier = new AkanVerifier2(this.#options.cwd);
|
|
545
|
+
pi.registerTool({
|
|
546
|
+
name: AkanToolPack2.verifyToolName,
|
|
547
|
+
label: "akan verify",
|
|
548
|
+
description: "Run the akan validation chain (sync, lint, typecheck, and quality ssr for .tsx changes) over everything the working tree has changed, and report what failed. Call this after editing before you finish.",
|
|
549
|
+
promptSnippet: "akan_verify: run sync/lint/typecheck/quality over the working tree's changes",
|
|
550
|
+
parameters: Type.Object({}),
|
|
551
|
+
execute: async () => {
|
|
552
|
+
const scope = await AkanEditScope2.since(this.#options.cwd, new Set);
|
|
553
|
+
const report = await verifier.verify(scope);
|
|
554
|
+
return {
|
|
555
|
+
content: [{ type: "text", text: AkanVerifier2.summarize(report) }],
|
|
556
|
+
details: undefined,
|
|
557
|
+
isError: !report.ok
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
static async#guidelineNames() {
|
|
563
|
+
try {
|
|
564
|
+
const { Prompter } = await import("./prompter-9mn3kp36.js");
|
|
565
|
+
return await Prompter.listGuidelines();
|
|
566
|
+
} catch {
|
|
567
|
+
return [];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
static #render(result) {
|
|
571
|
+
if (typeof result === "string")
|
|
572
|
+
return result;
|
|
573
|
+
return JSON.stringify(result, null, 2);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/MailToolPack.ts
|
|
578
|
+
import { Type as Type2 } from "typebox";
|
|
579
|
+
|
|
580
|
+
class MailToolPack2 {
|
|
581
|
+
static toolNames = ["list_peers", "send_peer_message"];
|
|
582
|
+
#options;
|
|
583
|
+
constructor(options) {
|
|
584
|
+
this.#options = options;
|
|
585
|
+
}
|
|
586
|
+
names() {
|
|
587
|
+
return this.#options.profile.session.store === "file" ? MailToolPack2.toolNames : [];
|
|
588
|
+
}
|
|
589
|
+
extension() {
|
|
590
|
+
if (!this.names().length)
|
|
591
|
+
return;
|
|
592
|
+
return { name: "akan-mail", factory: (pi) => this.#register(pi) };
|
|
593
|
+
}
|
|
594
|
+
#register(pi) {
|
|
595
|
+
pi.registerTool({
|
|
596
|
+
name: "list_peers",
|
|
597
|
+
label: "List peer sessions",
|
|
598
|
+
description: "List the other akan code sessions running in this workspace right now, with their name and working directory. Call it before send_peer_message so you name a session that is actually there.",
|
|
599
|
+
promptSnippet: "list_peers: the other akan code sessions running here",
|
|
600
|
+
parameters: Type2.Object({}),
|
|
601
|
+
execute: async () => ({
|
|
602
|
+
content: [{ type: "text", text: MailToolPack2.#render(this.#options.mailbox()?.peers() ?? []) }],
|
|
603
|
+
details: undefined
|
|
604
|
+
})
|
|
605
|
+
});
|
|
606
|
+
pi.registerTool({
|
|
607
|
+
name: "send_peer_message",
|
|
608
|
+
label: "Message a peer session",
|
|
609
|
+
description: "Hand a message to another akan code session in this workspace. It arrives there as a prompt, so it costs that session a turn \u2014 use it to hand over a piece of work or to warn it about a change you made, not to chat. Say what you want done and what you already did.",
|
|
610
|
+
promptSnippet: "send_peer_message: hand work to another akan code session",
|
|
611
|
+
parameters: Type2.Object({
|
|
612
|
+
to: Type2.String({ description: "The peer's name, or the front of its id, from list_peers." }),
|
|
613
|
+
message: Type2.String({ description: "What that session should know or do." })
|
|
614
|
+
}),
|
|
615
|
+
execute: async (_id, params) => {
|
|
616
|
+
const mailbox = this.#options.mailbox();
|
|
617
|
+
const peer = mailbox?.find(params.to);
|
|
618
|
+
if (!mailbox || !peer)
|
|
619
|
+
return {
|
|
620
|
+
content: [{ type: "text", text: `No live session matches "${params.to}". Call list_peers.` }],
|
|
621
|
+
details: undefined,
|
|
622
|
+
isError: true
|
|
623
|
+
};
|
|
624
|
+
try {
|
|
625
|
+
mailbox.send(peer.id, params.message);
|
|
626
|
+
return { content: [{ type: "text", text: `Sent to ${peer.name}.` }], details: undefined };
|
|
627
|
+
} catch (error) {
|
|
628
|
+
return { content: [{ type: "text", text: String(error) }], details: undefined, isError: true };
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
static #render(peers) {
|
|
634
|
+
if (!peers.length)
|
|
635
|
+
return "No other akan code session is running in this workspace.";
|
|
636
|
+
return peers.map((peer) => `${peer.name} (${peer.id.slice(0, 8)}) \u2014 ${peer.cwd}`).join(`
|
|
637
|
+
`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/McpToolPack.ts
|
|
642
|
+
import { existsSync as existsSync5, readFileSync } from "fs";
|
|
643
|
+
import { codeAgentClip, codeAgentOutputChars } from "akanjs/common";
|
|
644
|
+
import { Type as Type3 } from "typebox";
|
|
645
|
+
|
|
646
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/akanCodePaths.ts
|
|
647
|
+
import { existsSync as existsSync4 } from "fs";
|
|
648
|
+
import { homedir as homedir2 } from "os";
|
|
649
|
+
import path4 from "path";
|
|
650
|
+
var builtinSkillsDir = () => {
|
|
651
|
+
const candidates = [path4.join(import.meta.dir, "skills"), path4.join(import.meta.dir, "..", "skills")];
|
|
652
|
+
return candidates.find((candidate) => existsSync4(candidate));
|
|
653
|
+
};
|
|
654
|
+
var akanCodePaths2 = {
|
|
655
|
+
workspaceDir: (workspaceRoot) => path4.join(workspaceRoot, ".akan", "code"),
|
|
656
|
+
sessionsDir: (workspaceRoot) => path4.join(workspaceRoot, ".akan", "code", "sessions"),
|
|
657
|
+
extensionsDir: (workspaceRoot) => path4.join(workspaceRoot, ".akan", "code", "extensions"),
|
|
658
|
+
skillsDir: (workspaceRoot) => path4.join(workspaceRoot, ".akan", "code", "skills"),
|
|
659
|
+
mailDir: (workspaceRoot) => path4.join(workspaceRoot, ".akan", "code", "mail"),
|
|
660
|
+
builtinSkillsDir,
|
|
661
|
+
mcpFile: (workspaceRoot) => path4.join(workspaceRoot, ".akan", "code", "mcp.json"),
|
|
662
|
+
globalDir: () => path4.join(homedir2(), ".akan", "code"),
|
|
663
|
+
authFile: () => path4.join(homedir2(), ".akan", "code", "auth.json"),
|
|
664
|
+
modelsFile: () => path4.join(homedir2(), ".akan", "code", "models.json")
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/McpClient.ts
|
|
668
|
+
var protocolVersion = "2025-06-18";
|
|
669
|
+
|
|
670
|
+
class McpClient2 {
|
|
671
|
+
ref;
|
|
672
|
+
#proc;
|
|
673
|
+
#nextId = 0;
|
|
674
|
+
#buffer = "";
|
|
675
|
+
#pending = new Map;
|
|
676
|
+
#sessionId;
|
|
677
|
+
constructor(ref) {
|
|
678
|
+
this.ref = ref;
|
|
679
|
+
}
|
|
680
|
+
async connect() {
|
|
681
|
+
if (this.ref.transport === "stdio")
|
|
682
|
+
this.#spawn();
|
|
683
|
+
const initialize = await this.#call("initialize", {
|
|
684
|
+
protocolVersion,
|
|
685
|
+
capabilities: {},
|
|
686
|
+
clientInfo: { name: "akan-code", version: "1" }
|
|
687
|
+
});
|
|
688
|
+
if (initialize.error)
|
|
689
|
+
throw new Error(initialize.error.message);
|
|
690
|
+
await this.#notify("notifications/initialized");
|
|
691
|
+
return this;
|
|
692
|
+
}
|
|
693
|
+
async listTools() {
|
|
694
|
+
const response = await this.#call("tools/list", {});
|
|
695
|
+
if (response.error)
|
|
696
|
+
throw new Error(response.error.message);
|
|
697
|
+
return response.result?.tools ?? [];
|
|
698
|
+
}
|
|
699
|
+
async callTool(name, args) {
|
|
700
|
+
const response = await this.#call("tools/call", { name, arguments: args });
|
|
701
|
+
if (response.error)
|
|
702
|
+
return { text: response.error.message, isError: true };
|
|
703
|
+
return { text: McpClient2.#renderContent(response.result?.content), isError: !!response.result?.isError };
|
|
704
|
+
}
|
|
705
|
+
close() {
|
|
706
|
+
this.#proc?.kill();
|
|
707
|
+
this.#proc = undefined;
|
|
708
|
+
for (const resolve of this.#pending.values())
|
|
709
|
+
resolve({ error: { code: -1, message: "MCP server closed" } });
|
|
710
|
+
this.#pending.clear();
|
|
711
|
+
}
|
|
712
|
+
#spawn() {
|
|
713
|
+
if (!this.ref.command)
|
|
714
|
+
throw new Error(`MCP server "${this.ref.name}" declares no command`);
|
|
715
|
+
this.#proc = Bun.spawn([this.ref.command, ...this.ref.args ?? []], {
|
|
716
|
+
stdin: "pipe",
|
|
717
|
+
stdout: "pipe",
|
|
718
|
+
stderr: "ignore",
|
|
719
|
+
env: { ...process.env, ...this.ref.env }
|
|
720
|
+
});
|
|
721
|
+
this.#readStdout();
|
|
722
|
+
}
|
|
723
|
+
async#readStdout() {
|
|
724
|
+
const decoder = new TextDecoder;
|
|
725
|
+
for await (const chunk of this.#proc?.stdout ?? []) {
|
|
726
|
+
this.#buffer += decoder.decode(chunk, { stream: true });
|
|
727
|
+
let index = this.#buffer.indexOf(`
|
|
728
|
+
`);
|
|
729
|
+
while (index >= 0) {
|
|
730
|
+
const line = this.#buffer.slice(0, index).trim();
|
|
731
|
+
this.#buffer = this.#buffer.slice(index + 1);
|
|
732
|
+
if (line)
|
|
733
|
+
this.#receive(line);
|
|
734
|
+
index = this.#buffer.indexOf(`
|
|
735
|
+
`);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
#receive(line) {
|
|
740
|
+
try {
|
|
741
|
+
const message = JSON.parse(line);
|
|
742
|
+
if (typeof message.id !== "number")
|
|
743
|
+
return;
|
|
744
|
+
this.#pending.get(message.id)?.(message);
|
|
745
|
+
this.#pending.delete(message.id);
|
|
746
|
+
} catch {}
|
|
747
|
+
}
|
|
748
|
+
async#call(method, params) {
|
|
749
|
+
this.#nextId += 1;
|
|
750
|
+
const id = this.#nextId;
|
|
751
|
+
const request = { jsonrpc: "2.0", id, method, params };
|
|
752
|
+
if (this.ref.transport === "http")
|
|
753
|
+
return await this.#post(request);
|
|
754
|
+
this.#proc?.stdin.write(`${JSON.stringify(request)}
|
|
755
|
+
`);
|
|
756
|
+
return await new Promise((resolve) => {
|
|
757
|
+
this.#pending.set(id, resolve);
|
|
758
|
+
setTimeout(() => {
|
|
759
|
+
if (!this.#pending.delete(id))
|
|
760
|
+
return;
|
|
761
|
+
resolve({ error: { code: -2, message: `MCP server "${this.ref.name}" timed out on ${method}` } });
|
|
762
|
+
}, 30000);
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
async#notify(method) {
|
|
766
|
+
const request = { jsonrpc: "2.0", method };
|
|
767
|
+
if (this.ref.transport === "http")
|
|
768
|
+
await this.#post(request);
|
|
769
|
+
else
|
|
770
|
+
this.#proc?.stdin.write(`${JSON.stringify(request)}
|
|
771
|
+
`);
|
|
772
|
+
}
|
|
773
|
+
async#post(body) {
|
|
774
|
+
if (!this.ref.url)
|
|
775
|
+
throw new Error(`MCP server "${this.ref.name}" declares no url`);
|
|
776
|
+
const response = await fetch(this.ref.url, {
|
|
777
|
+
method: "POST",
|
|
778
|
+
headers: {
|
|
779
|
+
"content-type": "application/json",
|
|
780
|
+
accept: "application/json, text/event-stream",
|
|
781
|
+
...this.#sessionId ? { "mcp-session-id": this.#sessionId } : {}
|
|
782
|
+
},
|
|
783
|
+
body: JSON.stringify(body),
|
|
784
|
+
signal: AbortSignal.timeout(30000)
|
|
785
|
+
});
|
|
786
|
+
this.#sessionId = response.headers.get("mcp-session-id") ?? this.#sessionId;
|
|
787
|
+
const text = await response.text();
|
|
788
|
+
if (!text.trim())
|
|
789
|
+
return {};
|
|
790
|
+
const payload = text.startsWith("event:") || text.startsWith("data:") ? McpClient2.#firstSseData(text) : text;
|
|
791
|
+
return payload ? JSON.parse(payload) : {};
|
|
792
|
+
}
|
|
793
|
+
static #firstSseData(text) {
|
|
794
|
+
return text.split(`
|
|
795
|
+
`).find((line) => line.startsWith("data:"))?.slice(5).trim();
|
|
796
|
+
}
|
|
797
|
+
static #renderContent(content) {
|
|
798
|
+
if (!Array.isArray(content))
|
|
799
|
+
return typeof content === "string" ? content : JSON.stringify(content ?? null);
|
|
800
|
+
return content.map((part) => part.type === "text" ? part.text ?? "" : JSON.stringify(part)).join(`
|
|
801
|
+
`);
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/McpToolPack.ts
|
|
806
|
+
class McpToolPack2 {
|
|
807
|
+
#options;
|
|
808
|
+
#clients = [];
|
|
809
|
+
constructor(options) {
|
|
810
|
+
this.#options = options;
|
|
811
|
+
}
|
|
812
|
+
static async connect(options) {
|
|
813
|
+
const refs = McpToolPack2.#refs(options);
|
|
814
|
+
if (!refs.length)
|
|
815
|
+
return;
|
|
816
|
+
const pack = new McpToolPack2(options);
|
|
817
|
+
for (const ref of refs) {
|
|
818
|
+
try {
|
|
819
|
+
const client = await new McpClient2(ref).connect();
|
|
820
|
+
pack.#clients.push({ client, tools: await client.listTools() });
|
|
821
|
+
} catch (error) {
|
|
822
|
+
options.onNotice?.(`MCP server "${ref.name}" is unavailable: ${String(error)}`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return pack.#clients.length ? pack : undefined;
|
|
826
|
+
}
|
|
827
|
+
get toolNames() {
|
|
828
|
+
return this.#clients.flatMap(({ client, tools }) => tools.map((tool) => McpToolPack2.#nameOf(client.ref.name, tool.name)));
|
|
829
|
+
}
|
|
830
|
+
extension() {
|
|
831
|
+
return { name: "akan-mcp", factory: (pi) => this.#register(pi) };
|
|
832
|
+
}
|
|
833
|
+
close() {
|
|
834
|
+
for (const { client } of this.#clients)
|
|
835
|
+
client.close();
|
|
836
|
+
}
|
|
837
|
+
#register(pi) {
|
|
838
|
+
for (const { client, tools } of this.#clients)
|
|
839
|
+
for (const tool of tools) {
|
|
840
|
+
const name = McpToolPack2.#nameOf(client.ref.name, tool.name);
|
|
841
|
+
const description = tool.description ?? tool.name;
|
|
842
|
+
pi.registerTool({
|
|
843
|
+
name,
|
|
844
|
+
label: `${client.ref.name}: ${tool.name}`,
|
|
845
|
+
description,
|
|
846
|
+
promptSnippet: `${name}: ${description.split(".")[0] ?? description}`,
|
|
847
|
+
parameters: Type3.Unsafe(tool.inputSchema),
|
|
848
|
+
execute: async (_id, params) => {
|
|
849
|
+
const result = await client.callTool(tool.name, params ?? {});
|
|
850
|
+
return {
|
|
851
|
+
content: [{ type: "text", text: codeAgentClip(result.text, codeAgentOutputChars) }],
|
|
852
|
+
details: undefined,
|
|
853
|
+
isError: result.isError
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
static #nameOf(server, tool) {
|
|
860
|
+
return `mcp__${server.replace(/[^a-zA-Z0-9_]/g, "_")}__${tool}`;
|
|
861
|
+
}
|
|
862
|
+
static #refs(options) {
|
|
863
|
+
if (options.profile.tools.mcp === "off")
|
|
864
|
+
return [];
|
|
865
|
+
const declared = Array.isArray(options.profile.tools.mcp) ? options.profile.tools.mcp : [];
|
|
866
|
+
return [...McpToolPack2.#fromFile(options.workspaceRoot), ...declared];
|
|
867
|
+
}
|
|
868
|
+
static #fromFile(workspaceRoot) {
|
|
869
|
+
const file = akanCodePaths2.mcpFile(workspaceRoot);
|
|
870
|
+
if (!existsSync5(file))
|
|
871
|
+
return [];
|
|
872
|
+
try {
|
|
873
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
874
|
+
const entries = Object.entries(parsed.mcpServers ?? parsed.servers ?? {});
|
|
875
|
+
return entries.filter(([, entry]) => !entry.disabled).map(([name, entry]) => {
|
|
876
|
+
const transport = entry.url ? "http" : "stdio";
|
|
877
|
+
return {
|
|
878
|
+
name,
|
|
879
|
+
transport,
|
|
880
|
+
...entry.command ? { command: entry.command } : {},
|
|
881
|
+
...entry.args ? { args: entry.args } : {},
|
|
882
|
+
...entry.url ? { url: entry.url } : {},
|
|
883
|
+
...entry.env ? { env: entry.env } : {}
|
|
884
|
+
};
|
|
885
|
+
});
|
|
886
|
+
} catch {
|
|
887
|
+
return [];
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/SessionToolPack.ts
|
|
893
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
894
|
+
import { codeAgentClip as codeAgentClip2, codeAgentOutputChars as codeAgentOutputChars2 } from "akanjs/common";
|
|
895
|
+
import { Type as Type4 } from "typebox";
|
|
896
|
+
var snippetChars = 240;
|
|
897
|
+
|
|
898
|
+
class SessionToolPack2 {
|
|
899
|
+
#options;
|
|
900
|
+
constructor(options) {
|
|
901
|
+
this.#options = options;
|
|
902
|
+
}
|
|
903
|
+
static toolNames = ["session_search", "session_read"];
|
|
904
|
+
names() {
|
|
905
|
+
return this.#options.profile.session.crossSession ? SessionToolPack2.toolNames : [];
|
|
906
|
+
}
|
|
907
|
+
extension() {
|
|
908
|
+
if (!this.names().length)
|
|
909
|
+
return;
|
|
910
|
+
return { name: "akan-sessions", factory: (pi) => this.#register(pi) };
|
|
911
|
+
}
|
|
912
|
+
#register(pi) {
|
|
913
|
+
pi.registerTool({
|
|
914
|
+
name: "session_search",
|
|
915
|
+
label: "Search sessions",
|
|
916
|
+
description: "Search earlier coding-agent conversations in this workspace for a phrase, and get back the matching sessions with a snippet. Use it when the user refers to something decided before, or when you suspect this problem has been solved here already.",
|
|
917
|
+
promptSnippet: "session_search: find earlier conversations in this workspace by phrase",
|
|
918
|
+
parameters: Type4.Object({
|
|
919
|
+
query: Type4.String({ description: "A phrase to look for, matched case-insensitively." }),
|
|
920
|
+
limit: Type4.Optional(Type4.Number({ description: "How many sessions to return, default 5." }))
|
|
921
|
+
}),
|
|
922
|
+
execute: async (_id, params) => {
|
|
923
|
+
const matches = await this.#search(params.query, params.limit ?? 5);
|
|
924
|
+
return { content: [{ type: "text", text: matches }], details: undefined };
|
|
925
|
+
}
|
|
926
|
+
});
|
|
927
|
+
pi.registerTool({
|
|
928
|
+
name: "session_read",
|
|
929
|
+
label: "Read session",
|
|
930
|
+
description: "Read one earlier conversation, found with session_search, as plain text.",
|
|
931
|
+
promptSnippet: "session_read: read one earlier conversation by id",
|
|
932
|
+
parameters: Type4.Object({ sessionId: Type4.String() }),
|
|
933
|
+
execute: async (_id, params) => {
|
|
934
|
+
const text = await this.#read(params.sessionId);
|
|
935
|
+
return { content: [{ type: "text", text }], details: undefined, isError: text.startsWith("No session") };
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
async#search(query, limit) {
|
|
940
|
+
const needle = query.toLowerCase();
|
|
941
|
+
const sessions = (await this.#list()).filter((session) => session.id !== this.#options.currentSessionId() && session.allMessagesText.toLowerCase().includes(needle));
|
|
942
|
+
if (!sessions.length)
|
|
943
|
+
return `No earlier session mentions "${query}".`;
|
|
944
|
+
return sessions.slice(0, limit).map((session) => {
|
|
945
|
+
const at = session.allMessagesText.toLowerCase().indexOf(needle);
|
|
946
|
+
const snippet = session.allMessagesText.slice(Math.max(0, at - 80), at + snippetChars).replace(/\s+/g, " ");
|
|
947
|
+
return `- ${session.id} (${session.modified.toISOString().slice(0, 10)}${session.name ? `, "${session.name}"` : ""})
|
|
948
|
+
\u2026${snippet}\u2026`;
|
|
949
|
+
}).join(`
|
|
950
|
+
`);
|
|
951
|
+
}
|
|
952
|
+
async#read(sessionId) {
|
|
953
|
+
const session = (await this.#list()).find((entry) => entry.id === sessionId);
|
|
954
|
+
if (!session)
|
|
955
|
+
return `No session with id ${sessionId} in this workspace.`;
|
|
956
|
+
return codeAgentClip2(session.allMessagesText, codeAgentOutputChars2 * 2);
|
|
957
|
+
}
|
|
958
|
+
async#list() {
|
|
959
|
+
return await SessionManager.list(this.#options.cwd, akanCodePaths2.sessionsDir(this.#options.workspaceRoot));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/WebToolPack.ts
|
|
964
|
+
import { codeAgentClip as codeAgentClip3, codeAgentOutputChars as codeAgentOutputChars3 } from "akanjs/common";
|
|
965
|
+
import { Type as Type5 } from "typebox";
|
|
966
|
+
|
|
967
|
+
class WebToolPack2 {
|
|
968
|
+
#options;
|
|
969
|
+
constructor(options) {
|
|
970
|
+
this.#options = options;
|
|
971
|
+
}
|
|
972
|
+
static providers = [
|
|
973
|
+
{
|
|
974
|
+
name: "brave",
|
|
975
|
+
envKey: "BRAVE_API_KEY",
|
|
976
|
+
search: async (key, query, count) => {
|
|
977
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
|
|
978
|
+
const response = await fetch(url, { headers: { "x-subscription-token": key, accept: "application/json" } });
|
|
979
|
+
const body = await response.json();
|
|
980
|
+
return (body.web?.results ?? []).map((r) => `- ${r.title}
|
|
981
|
+
${r.url}
|
|
982
|
+
${r.description}`).join(`
|
|
983
|
+
`);
|
|
984
|
+
}
|
|
985
|
+
},
|
|
986
|
+
{
|
|
987
|
+
name: "tavily",
|
|
988
|
+
envKey: "TAVILY_API_KEY",
|
|
989
|
+
search: async (key, query, count) => {
|
|
990
|
+
const response = await fetch("https://api.tavily.com/search", {
|
|
991
|
+
method: "POST",
|
|
992
|
+
headers: { "content-type": "application/json" },
|
|
993
|
+
body: JSON.stringify({ api_key: key, query, max_results: count })
|
|
994
|
+
});
|
|
995
|
+
const body = await response.json();
|
|
996
|
+
return (body.results ?? []).map((r) => `- ${r.title}
|
|
997
|
+
${r.url}
|
|
998
|
+
${r.content}`).join(`
|
|
999
|
+
`);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
];
|
|
1003
|
+
names() {
|
|
1004
|
+
const names = [];
|
|
1005
|
+
if (this.#options.profile.tools.web.fetch)
|
|
1006
|
+
names.push("web_fetch");
|
|
1007
|
+
if (this.#options.profile.tools.web.search && WebToolPack2.#provider())
|
|
1008
|
+
names.push("web_search");
|
|
1009
|
+
return names;
|
|
1010
|
+
}
|
|
1011
|
+
extension() {
|
|
1012
|
+
if (!this.names().length)
|
|
1013
|
+
return;
|
|
1014
|
+
return { name: "akan-web", factory: (pi) => this.#register(pi) };
|
|
1015
|
+
}
|
|
1016
|
+
#register(pi) {
|
|
1017
|
+
if (this.#options.profile.tools.web.fetch)
|
|
1018
|
+
this.#registerFetch(pi);
|
|
1019
|
+
const provider = WebToolPack2.#provider();
|
|
1020
|
+
if (this.#options.profile.tools.web.search && provider)
|
|
1021
|
+
this.#registerSearch(pi, provider);
|
|
1022
|
+
}
|
|
1023
|
+
#registerFetch(pi) {
|
|
1024
|
+
const allowHosts = this.#options.profile.network.allowHosts;
|
|
1025
|
+
pi.registerTool({
|
|
1026
|
+
name: "web_fetch",
|
|
1027
|
+
label: "Fetch",
|
|
1028
|
+
description: "Fetch a URL and return its readable text. HTML is stripped to text; the result is truncated.",
|
|
1029
|
+
promptSnippet: "web_fetch: fetch a URL and return its readable text",
|
|
1030
|
+
parameters: Type5.Object({ url: Type5.String({ description: "Absolute http(s) URL." }) }),
|
|
1031
|
+
execute: async (_id, params) => {
|
|
1032
|
+
const refusal = WebToolPack2.#refuseHost(params.url, allowHosts);
|
|
1033
|
+
if (refusal)
|
|
1034
|
+
return { content: [{ type: "text", text: refusal }], details: undefined, isError: true };
|
|
1035
|
+
const response = await fetch(params.url, { signal: AbortSignal.timeout(20000), redirect: "follow" });
|
|
1036
|
+
const text = WebToolPack2.#readable(await response.text());
|
|
1037
|
+
return {
|
|
1038
|
+
content: [
|
|
1039
|
+
{ type: "text", text: `${response.status} ${params.url}
|
|
1040
|
+
|
|
1041
|
+
${codeAgentClip3(text, codeAgentOutputChars3)}` }
|
|
1042
|
+
],
|
|
1043
|
+
details: undefined,
|
|
1044
|
+
isError: !response.ok
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
#registerSearch(pi, provider) {
|
|
1050
|
+
pi.registerTool({
|
|
1051
|
+
name: "web_search",
|
|
1052
|
+
label: "Search",
|
|
1053
|
+
description: `Search the web (${provider.name}) and return the top results as title, url and snippet.`,
|
|
1054
|
+
promptSnippet: "web_search: search the web and return titles, urls and snippets",
|
|
1055
|
+
parameters: Type5.Object({
|
|
1056
|
+
query: Type5.String(),
|
|
1057
|
+
count: Type5.Optional(Type5.Number({ description: "How many results, default 5." }))
|
|
1058
|
+
}),
|
|
1059
|
+
execute: async (_id, params) => {
|
|
1060
|
+
const key = process.env[provider.envKey] ?? "";
|
|
1061
|
+
const results = await provider.search(key, params.query, params.count ?? 5);
|
|
1062
|
+
return {
|
|
1063
|
+
content: [{ type: "text", text: codeAgentClip3(results || "(no results)", codeAgentOutputChars3) }],
|
|
1064
|
+
details: undefined
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
static #provider() {
|
|
1070
|
+
return WebToolPack2.providers.find((provider) => !!process.env[provider.envKey]);
|
|
1071
|
+
}
|
|
1072
|
+
static #refuseHost(url, allowHosts) {
|
|
1073
|
+
let parsed;
|
|
1074
|
+
try {
|
|
1075
|
+
parsed = new URL(url);
|
|
1076
|
+
} catch {
|
|
1077
|
+
return `"${url}" is not an absolute URL.`;
|
|
1078
|
+
}
|
|
1079
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
1080
|
+
return "Only http and https URLs can be fetched.";
|
|
1081
|
+
if (allowHosts?.length && !allowHosts.includes(parsed.hostname))
|
|
1082
|
+
return `This profile only allows ${allowHosts.join(", ")}.`;
|
|
1083
|
+
return;
|
|
1084
|
+
}
|
|
1085
|
+
static #readable(html) {
|
|
1086
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, '"').replace(/[ \t]+/g, " ").replace(/\n{3,}/g, `
|
|
1087
|
+
|
|
1088
|
+
`).trim();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanCodePlugins.ts
|
|
1093
|
+
class AkanCodePlugins2 {
|
|
1094
|
+
static async build(options) {
|
|
1095
|
+
const mcp = await McpToolPack2.connect({
|
|
1096
|
+
workspaceRoot: options.workspace.workspaceRoot,
|
|
1097
|
+
profile: options.profile,
|
|
1098
|
+
...options.onNotice ? { onNotice: options.onNotice } : {}
|
|
1099
|
+
});
|
|
1100
|
+
const dispose = () => mcp?.close();
|
|
1101
|
+
const web = new WebToolPack2({ profile: options.profile });
|
|
1102
|
+
const webExtension = web.extension();
|
|
1103
|
+
const sessions = new SessionToolPack2({
|
|
1104
|
+
workspaceRoot: options.workspace.workspaceRoot,
|
|
1105
|
+
cwd: options.cwd,
|
|
1106
|
+
profile: options.profile,
|
|
1107
|
+
currentSessionId: options.currentSessionId
|
|
1108
|
+
});
|
|
1109
|
+
const sessionExtension = sessions.extension();
|
|
1110
|
+
const mail = new MailToolPack2({ profile: options.profile, mailbox: options.mailbox });
|
|
1111
|
+
const mailExtension = mail.extension();
|
|
1112
|
+
if (!options.profile.tools.akan)
|
|
1113
|
+
return {
|
|
1114
|
+
extensions: [mcp?.extension(), webExtension, sessionExtension, mailExtension].filter((entry) => !!entry),
|
|
1115
|
+
toolNames: [...mcp?.toolNames ?? [], ...web.names(), ...sessions.names(), ...mail.names()],
|
|
1116
|
+
dispose
|
|
1117
|
+
};
|
|
1118
|
+
const readOnly = !options.profile.tools.builtin.includes("write");
|
|
1119
|
+
const pack = new AkanToolPack2({
|
|
1120
|
+
workspace: options.workspace,
|
|
1121
|
+
cwd: options.cwd,
|
|
1122
|
+
mode: readOnly ? "readonly" : "apply"
|
|
1123
|
+
});
|
|
1124
|
+
const extensions = [pack.extension()];
|
|
1125
|
+
const subagent = SubagentPool2.extensionFor({
|
|
1126
|
+
workspace: options.workspace,
|
|
1127
|
+
cwd: options.cwd,
|
|
1128
|
+
parent: options.profile,
|
|
1129
|
+
depth: options.depth,
|
|
1130
|
+
...options.onNotice ? { onNotice: options.onNotice } : {}
|
|
1131
|
+
});
|
|
1132
|
+
if (subagent)
|
|
1133
|
+
extensions.push(subagent);
|
|
1134
|
+
const sources = await AkanCodePlugins2.#sources(options, readOnly);
|
|
1135
|
+
if (sources.length)
|
|
1136
|
+
extensions.push(new TurnFeedback2(sources, {
|
|
1137
|
+
budget: options.profile.limits.feedback,
|
|
1138
|
+
...options.onNotice ? { onNotice: options.onNotice } : {}
|
|
1139
|
+
}).extension());
|
|
1140
|
+
if (mcp)
|
|
1141
|
+
extensions.push(mcp.extension());
|
|
1142
|
+
if (webExtension)
|
|
1143
|
+
extensions.push(webExtension);
|
|
1144
|
+
if (sessionExtension)
|
|
1145
|
+
extensions.push(sessionExtension);
|
|
1146
|
+
if (mailExtension)
|
|
1147
|
+
extensions.push(mailExtension);
|
|
1148
|
+
return {
|
|
1149
|
+
extensions,
|
|
1150
|
+
toolNames: [
|
|
1151
|
+
...await pack.names(),
|
|
1152
|
+
...subagent ? [SubagentPool2.toolName] : [],
|
|
1153
|
+
...mcp?.toolNames ?? [],
|
|
1154
|
+
...web.names(),
|
|
1155
|
+
...sessions.names(),
|
|
1156
|
+
...mail.names()
|
|
1157
|
+
],
|
|
1158
|
+
dispose
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
static async#sources(options, readOnly) {
|
|
1162
|
+
const sources = [
|
|
1163
|
+
new VerifyFeedback2({ cwd: options.cwd, readOnly }),
|
|
1164
|
+
new DevLogFeedback2({ workspaceRoot: options.workspace.workspaceRoot, cwd: options.cwd, apps: options.apps })
|
|
1165
|
+
];
|
|
1166
|
+
const previewUrl = await AkanCodePlugins2.#previewUrl(options);
|
|
1167
|
+
if (!previewUrl)
|
|
1168
|
+
return sources;
|
|
1169
|
+
const reason = PreviewView2.unavailableReason();
|
|
1170
|
+
if (reason)
|
|
1171
|
+
options.onNotice?.(reason);
|
|
1172
|
+
else
|
|
1173
|
+
sources.push(new PreviewFeedback2({ cwd: options.cwd, previewUrl, canSeeImages: options.canSeeImages }));
|
|
1174
|
+
return sources;
|
|
1175
|
+
}
|
|
1176
|
+
static async#previewUrl(options) {
|
|
1177
|
+
for (const app of options.apps) {
|
|
1178
|
+
const url = await DevLogFeedback2.previewUrl(options.workspace.workspaceRoot, app);
|
|
1179
|
+
if (url)
|
|
1180
|
+
return url;
|
|
1181
|
+
}
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/AkanCodeServices.ts
|
|
1187
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync2 } from "fs";
|
|
1188
|
+
import {
|
|
1189
|
+
AuthStorage,
|
|
1190
|
+
ModelRegistry,
|
|
1191
|
+
SessionManager as SessionManager2,
|
|
1192
|
+
SettingsManager
|
|
1193
|
+
} from "@earendil-works/pi-coding-agent";
|
|
1194
|
+
|
|
1195
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanEnvKeys.ts
|
|
1196
|
+
import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
|
|
1197
|
+
import path5 from "path";
|
|
1198
|
+
|
|
1199
|
+
class AkanEnvKeys2 {
|
|
1200
|
+
static apply(authStorage, workspaceRoot) {
|
|
1201
|
+
const applied = [];
|
|
1202
|
+
for (const [name, value] of AkanEnvKeys2.#entries(workspaceRoot)) {
|
|
1203
|
+
const provider = AkanEnvKeys2.#providerOf(name);
|
|
1204
|
+
if (!provider || !value)
|
|
1205
|
+
continue;
|
|
1206
|
+
authStorage.setRuntimeApiKey(provider, value);
|
|
1207
|
+
applied.push(provider);
|
|
1208
|
+
}
|
|
1209
|
+
return applied;
|
|
1210
|
+
}
|
|
1211
|
+
static #entries(workspaceRoot) {
|
|
1212
|
+
const fromProcess = Object.entries(process.env).filter(([, value]) => typeof value === "string");
|
|
1213
|
+
const file = path5.join(workspaceRoot, ".env");
|
|
1214
|
+
if (!existsSync6(file))
|
|
1215
|
+
return fromProcess;
|
|
1216
|
+
const fromFile = readFileSync2(file, "utf8").split(`
|
|
1217
|
+
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).map((line) => {
|
|
1218
|
+
const index = line.indexOf("=");
|
|
1219
|
+
return [
|
|
1220
|
+
line.slice(0, index).trim(),
|
|
1221
|
+
line.slice(index + 1).trim().replace(/^["']|["']$/g, "")
|
|
1222
|
+
];
|
|
1223
|
+
});
|
|
1224
|
+
return [...fromFile, ...fromProcess];
|
|
1225
|
+
}
|
|
1226
|
+
static #providerOf(name) {
|
|
1227
|
+
const match = /^([A-Z0-9_]+)_API_KEY$/.exec(name);
|
|
1228
|
+
return match?.[1]?.toLowerCase().replace(/_/g, "-");
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/AkanAuthStorageBackend.ts
|
|
1233
|
+
import {
|
|
1234
|
+
closeSync,
|
|
1235
|
+
existsSync as existsSync7,
|
|
1236
|
+
mkdirSync,
|
|
1237
|
+
openSync,
|
|
1238
|
+
readFileSync as readFileSync3,
|
|
1239
|
+
renameSync,
|
|
1240
|
+
unlinkSync,
|
|
1241
|
+
writeFileSync
|
|
1242
|
+
} from "fs";
|
|
1243
|
+
import path6 from "path";
|
|
1244
|
+
|
|
1245
|
+
class AkanAuthStorageBackend2 {
|
|
1246
|
+
#authPath;
|
|
1247
|
+
#lockPath;
|
|
1248
|
+
constructor(authPath) {
|
|
1249
|
+
this.#authPath = authPath;
|
|
1250
|
+
this.#lockPath = `${authPath}.lock`;
|
|
1251
|
+
}
|
|
1252
|
+
withLock(fn) {
|
|
1253
|
+
this.#acquire();
|
|
1254
|
+
try {
|
|
1255
|
+
const { result, next } = fn(this.#read());
|
|
1256
|
+
if (next !== undefined)
|
|
1257
|
+
this.#write(next);
|
|
1258
|
+
return result;
|
|
1259
|
+
} finally {
|
|
1260
|
+
this.#release();
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
async withLockAsync(fn) {
|
|
1264
|
+
this.#acquire();
|
|
1265
|
+
try {
|
|
1266
|
+
const { result, next } = await fn(this.#read());
|
|
1267
|
+
if (next !== undefined)
|
|
1268
|
+
this.#write(next);
|
|
1269
|
+
return result;
|
|
1270
|
+
} finally {
|
|
1271
|
+
this.#release();
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
#read() {
|
|
1275
|
+
if (!existsSync7(this.#authPath))
|
|
1276
|
+
return;
|
|
1277
|
+
const text = readFileSync3(this.#authPath, "utf8");
|
|
1278
|
+
return text.trim() ? text : undefined;
|
|
1279
|
+
}
|
|
1280
|
+
#write(next) {
|
|
1281
|
+
mkdirSync(path6.dirname(this.#authPath), { recursive: true, mode: 448 });
|
|
1282
|
+
const temp = `${this.#authPath}.${process.pid}.tmp`;
|
|
1283
|
+
writeFileSync(temp, next, { mode: 384 });
|
|
1284
|
+
renameSync(temp, this.#authPath);
|
|
1285
|
+
}
|
|
1286
|
+
#acquire() {
|
|
1287
|
+
mkdirSync(path6.dirname(this.#lockPath), { recursive: true, mode: 448 });
|
|
1288
|
+
const deadline = Date.now() + 5000;
|
|
1289
|
+
for (;; ) {
|
|
1290
|
+
try {
|
|
1291
|
+
closeSync(openSync(this.#lockPath, "wx"));
|
|
1292
|
+
return;
|
|
1293
|
+
} catch {
|
|
1294
|
+
if (this.#lockIsStale() || Date.now() > deadline) {
|
|
1295
|
+
try {
|
|
1296
|
+
unlinkSync(this.#lockPath);
|
|
1297
|
+
} catch {}
|
|
1298
|
+
continue;
|
|
1299
|
+
}
|
|
1300
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
#lockIsStale() {
|
|
1305
|
+
try {
|
|
1306
|
+
return Date.now() - Bun.file(this.#lockPath).lastModified > 30000;
|
|
1307
|
+
} catch {
|
|
1308
|
+
return true;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
#release() {
|
|
1312
|
+
try {
|
|
1313
|
+
unlinkSync(this.#lockPath);
|
|
1314
|
+
} catch {}
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/akanSystemPrompt.ts
|
|
1319
|
+
var akanSystemPrompt2 = (profile) => {
|
|
1320
|
+
const lines = [
|
|
1321
|
+
"You are the akan coding agent, running inside an Akan.js workspace.",
|
|
1322
|
+
"",
|
|
1323
|
+
"- Prefer an akan workflow to a direct source edit. `list_workflows` / `explain_workflow` / `plan_workflow`, then `apply_workflow({ planPath })`. Editing source by hand is the fallback, not the default.",
|
|
1324
|
+
"- Read context with `inspect_akan_context` before reading source bodies; it answers most structural questions without spending the file.",
|
|
1325
|
+
"- Fetch the guideline for an area before a deep pass on it: `get_guideline`.",
|
|
1326
|
+
"- Never hand-edit a generated file (`lib/cnst.ts`, `db.ts`, `st.ts`, any `*/index.ts` barrel). Fix the owning source and run `repair_generated` or `akan sync`."
|
|
1327
|
+
];
|
|
1328
|
+
if (profile.tools.builtin.includes("write"))
|
|
1329
|
+
lines.push("- After editing, call `akan_verify`. It runs sync, lint, typecheck and the SSR check over what the working tree changed. A turn that edits and does not verify is not finished.");
|
|
1330
|
+
else
|
|
1331
|
+
lines.push("- This session is read-only: there is no write, edit or bash tool. Report what should change instead of changing it.");
|
|
1332
|
+
if (profile.tools.subagent)
|
|
1333
|
+
lines.push('- Hand a self-contained search or read to `task` \u2014 `type: "explore"` reads and reports, `type: "code"` makes the change. Only its answer comes back, so give it everything it needs and ask for exactly what you want returned. Do it yourself when the reading is short or when you need the files in front of you.');
|
|
1334
|
+
if (profile.approval !== "never")
|
|
1335
|
+
lines.push(`- Some tool calls need the user's approval (policy: ${profile.approval}). A refusal is an answer; do not retry it a different way.`);
|
|
1336
|
+
lines.push("- Secrets are refused at the tool boundary. If you need a value from `.env`, ask the user for it rather than looking for another way to read the file.", "", "## How to answer", "", "- Answer in as few words as carry the answer. A short question gets one sentence with no heading.", "- Do not open with what you are about to do, and do not close with a summary of what you did. The tool calls are already on the user's screen.", "- Skip headings, bold labels and bullet lists unless the answer is genuinely a list. Prose is shorter.", "- State a file as `path:line`. Do not paste a file back that the user can open.", "- Say what you could not do, and why, in one line. Do not offer alternatives nobody asked for.");
|
|
1337
|
+
return lines.join(`
|
|
1338
|
+
`);
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeSessionIndex.ts
|
|
1342
|
+
import { existsSync as existsSync8, readdirSync, readFileSync as readFileSync4, statSync as statSync2 } from "fs";
|
|
1343
|
+
import path7 from "path";
|
|
1344
|
+
|
|
1345
|
+
class CodeSessionIndex2 {
|
|
1346
|
+
static readLimit = 512 * 1024;
|
|
1347
|
+
static list(dir, limit = 30) {
|
|
1348
|
+
if (!existsSync8(dir))
|
|
1349
|
+
return [];
|
|
1350
|
+
const files = readdirSync(dir).filter((entry) => entry.endsWith(".jsonl")).map((entry) => path7.join(dir, entry)).map((file) => ({ file, updatedAt: CodeSessionIndex2.#mtime(file) })).sort((left, right) => right.updatedAt - left.updatedAt).slice(0, limit);
|
|
1351
|
+
return files.map((entry) => CodeSessionIndex2.#read(entry.file, entry.updatedAt)).filter((entry) => !!entry);
|
|
1352
|
+
}
|
|
1353
|
+
static fileOf(dir, id) {
|
|
1354
|
+
if (!existsSync8(dir))
|
|
1355
|
+
return;
|
|
1356
|
+
const match = readdirSync(dir).find((entry) => entry.endsWith(`${id}.jsonl`) || entry === `${id}.jsonl`);
|
|
1357
|
+
return match ? path7.join(dir, match) : undefined;
|
|
1358
|
+
}
|
|
1359
|
+
static #mtime(file) {
|
|
1360
|
+
try {
|
|
1361
|
+
return statSync2(file).mtimeMs;
|
|
1362
|
+
} catch {
|
|
1363
|
+
return 0;
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
static #read(file, updatedAt) {
|
|
1367
|
+
const lines = CodeSessionIndex2.#lines(file);
|
|
1368
|
+
if (!lines.length)
|
|
1369
|
+
return;
|
|
1370
|
+
const entry = { id: "", file, name: undefined, opening: "", updatedAt, turns: 0 };
|
|
1371
|
+
for (const line of lines) {
|
|
1372
|
+
const record = CodeSessionIndex2.#parse(line);
|
|
1373
|
+
if (!record)
|
|
1374
|
+
continue;
|
|
1375
|
+
if (record.type === "session" && typeof record.id === "string")
|
|
1376
|
+
entry.id = record.id;
|
|
1377
|
+
if (record.type === "session_info" && typeof record.name === "string")
|
|
1378
|
+
entry.name = record.name;
|
|
1379
|
+
if (record.type !== "message")
|
|
1380
|
+
continue;
|
|
1381
|
+
const message = record.message;
|
|
1382
|
+
if (message?.role !== "user")
|
|
1383
|
+
continue;
|
|
1384
|
+
entry.turns += 1;
|
|
1385
|
+
if (!entry.opening)
|
|
1386
|
+
entry.opening = CodeSessionIndex2.#text(message.content);
|
|
1387
|
+
}
|
|
1388
|
+
return entry.id ? entry : undefined;
|
|
1389
|
+
}
|
|
1390
|
+
static #lines(file) {
|
|
1391
|
+
try {
|
|
1392
|
+
if (statSync2(file).size > CodeSessionIndex2.readLimit)
|
|
1393
|
+
return [];
|
|
1394
|
+
return readFileSync4(file, "utf8").split(`
|
|
1395
|
+
`).filter((line) => !!line.trim());
|
|
1396
|
+
} catch {
|
|
1397
|
+
return [];
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
static #parse(line) {
|
|
1401
|
+
try {
|
|
1402
|
+
return JSON.parse(line);
|
|
1403
|
+
} catch {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
static #text(content) {
|
|
1408
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => typeof part === "object" && part && ("text" in part) ? String(part.text) : "").join(" ") : "";
|
|
1409
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1410
|
+
}
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/AkanCodeServices.ts
|
|
1414
|
+
class AkanCodeServices2 {
|
|
1415
|
+
static settings() {
|
|
1416
|
+
return SettingsManager.inMemory({
|
|
1417
|
+
enableInstallTelemetry: false,
|
|
1418
|
+
enableAnalytics: false,
|
|
1419
|
+
quietStartup: true
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
static auth(workspaceRoot) {
|
|
1423
|
+
mkdirSync2(akanCodePaths2.globalDir(), { recursive: true, mode: 448 });
|
|
1424
|
+
const authStorage = AuthStorage.fromStorage(new AkanAuthStorageBackend2(akanCodePaths2.authFile()));
|
|
1425
|
+
AkanEnvKeys2.apply(authStorage, workspaceRoot);
|
|
1426
|
+
return authStorage;
|
|
1427
|
+
}
|
|
1428
|
+
static models(authStorage) {
|
|
1429
|
+
return ModelRegistry.create(authStorage, akanCodePaths2.modelsFile());
|
|
1430
|
+
}
|
|
1431
|
+
static sessions(workspaceRoot, cwd, profile, resume) {
|
|
1432
|
+
if (profile.session.store !== "file")
|
|
1433
|
+
return SessionManager2.inMemory(cwd);
|
|
1434
|
+
const dir = akanCodePaths2.sessionsDir(workspaceRoot);
|
|
1435
|
+
mkdirSync2(dir, { recursive: true });
|
|
1436
|
+
const manager = SessionManager2.create(cwd, dir);
|
|
1437
|
+
if (!resume)
|
|
1438
|
+
return manager;
|
|
1439
|
+
const file = CodeSessionIndex2.fileOf(dir, resume);
|
|
1440
|
+
if (!file)
|
|
1441
|
+
throw new Error(`No stored session ${resume} in ${dir}`);
|
|
1442
|
+
manager.setSessionFile(file);
|
|
1443
|
+
return manager;
|
|
1444
|
+
}
|
|
1445
|
+
static #skillPaths(profile, workspaceRoot) {
|
|
1446
|
+
if (!profile.context.skills)
|
|
1447
|
+
return [];
|
|
1448
|
+
const workspace = akanCodePaths2.skillsDir(workspaceRoot);
|
|
1449
|
+
return [akanCodePaths2.builtinSkillsDir(), existsSync9(workspace) ? workspace : undefined].filter((dir) => !!dir);
|
|
1450
|
+
}
|
|
1451
|
+
static resourceOptions(options) {
|
|
1452
|
+
const present = (dir) => existsSync9(dir) ? [dir] : [];
|
|
1453
|
+
return {
|
|
1454
|
+
noExtensions: true,
|
|
1455
|
+
noSkills: true,
|
|
1456
|
+
noPromptTemplates: true,
|
|
1457
|
+
noThemes: true,
|
|
1458
|
+
noContextFiles: !options.profile.context.projectFiles,
|
|
1459
|
+
appendSystemPrompt: [akanSystemPrompt2(options.profile)],
|
|
1460
|
+
additionalExtensionPaths: present(akanCodePaths2.extensionsDir(options.workspaceRoot)),
|
|
1461
|
+
additionalSkillPaths: AkanCodeServices2.#skillPaths(options.profile, options.workspaceRoot),
|
|
1462
|
+
extensionFactories: options.extensions
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentAsks.ts
|
|
1468
|
+
class CodeAgentAsks2 {
|
|
1469
|
+
#question;
|
|
1470
|
+
#approvals = new Map;
|
|
1471
|
+
#nextId = 0;
|
|
1472
|
+
get pendingQuestionId() {
|
|
1473
|
+
return this.#question?.question.questionId;
|
|
1474
|
+
}
|
|
1475
|
+
questionOf(questionId) {
|
|
1476
|
+
return this.#question?.question.questionId === questionId ? this.#question.question : undefined;
|
|
1477
|
+
}
|
|
1478
|
+
get hasPendingApproval() {
|
|
1479
|
+
return this.#approvals.size > 0;
|
|
1480
|
+
}
|
|
1481
|
+
nextId(prefix) {
|
|
1482
|
+
this.#nextId += 1;
|
|
1483
|
+
return `${prefix}${this.#nextId}`;
|
|
1484
|
+
}
|
|
1485
|
+
openQuestion(question) {
|
|
1486
|
+
this.#question?.deferred.resolve("");
|
|
1487
|
+
const deferred = CodeAgentAsks2.#defer();
|
|
1488
|
+
this.#question = { question, deferred };
|
|
1489
|
+
return deferred.promise;
|
|
1490
|
+
}
|
|
1491
|
+
answer(id, text) {
|
|
1492
|
+
if (this.#question?.question.questionId !== id)
|
|
1493
|
+
return false;
|
|
1494
|
+
const { deferred } = this.#question;
|
|
1495
|
+
this.#question = undefined;
|
|
1496
|
+
deferred.resolve(text);
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
openApproval(id) {
|
|
1500
|
+
const deferred = CodeAgentAsks2.#defer();
|
|
1501
|
+
this.#approvals.set(id, deferred);
|
|
1502
|
+
return deferred.promise;
|
|
1503
|
+
}
|
|
1504
|
+
resolveApproval(id, approved) {
|
|
1505
|
+
const deferred = this.#approvals.get(id);
|
|
1506
|
+
if (!deferred)
|
|
1507
|
+
return false;
|
|
1508
|
+
this.#approvals.delete(id);
|
|
1509
|
+
deferred.resolve(approved);
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
clear() {
|
|
1513
|
+
this.#question?.deferred.resolve("");
|
|
1514
|
+
this.#question = undefined;
|
|
1515
|
+
for (const deferred of this.#approvals.values())
|
|
1516
|
+
deferred.resolve(false);
|
|
1517
|
+
this.#approvals.clear();
|
|
1518
|
+
}
|
|
1519
|
+
static #defer() {
|
|
1520
|
+
let resolve = () => {};
|
|
1521
|
+
const promise = new Promise((r) => {
|
|
1522
|
+
resolve = r;
|
|
1523
|
+
});
|
|
1524
|
+
return { resolve, promise };
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentEventMapper.ts
|
|
1529
|
+
import {
|
|
1530
|
+
codeAgentClip as codeAgentClip4,
|
|
1531
|
+
codeAgentLabelChars,
|
|
1532
|
+
codeAgentOutputChars as codeAgentOutputChars4
|
|
1533
|
+
} from "akanjs/common";
|
|
1534
|
+
var toolOps = {
|
|
1535
|
+
read: "read",
|
|
1536
|
+
write: "write",
|
|
1537
|
+
edit: "write",
|
|
1538
|
+
ls: "list",
|
|
1539
|
+
find: "search",
|
|
1540
|
+
grep: "search",
|
|
1541
|
+
bash: "execute"
|
|
1542
|
+
};
|
|
1543
|
+
|
|
1544
|
+
class CodeAgentEventMapper2 {
|
|
1545
|
+
#turn = 0;
|
|
1546
|
+
#turnId = "";
|
|
1547
|
+
#outcome;
|
|
1548
|
+
#openTools = new Map;
|
|
1549
|
+
#blocked = new Map;
|
|
1550
|
+
get turnId() {
|
|
1551
|
+
return this.#turnId;
|
|
1552
|
+
}
|
|
1553
|
+
map(event) {
|
|
1554
|
+
switch (event.type) {
|
|
1555
|
+
case "agent_start":
|
|
1556
|
+
this.#turn += 1;
|
|
1557
|
+
this.#turnId = `t${this.#turn}`;
|
|
1558
|
+
this.#outcome = undefined;
|
|
1559
|
+
return [{ type: "turn_start", turnId: this.#turnId }];
|
|
1560
|
+
case "agent_end":
|
|
1561
|
+
return [
|
|
1562
|
+
...this.flushBlocked(),
|
|
1563
|
+
{ type: "turn_end", turnId: this.#turnId, stopReason: this.#stopReason(event) }
|
|
1564
|
+
];
|
|
1565
|
+
case "agent_settled":
|
|
1566
|
+
return [{ type: "idle" }];
|
|
1567
|
+
case "message_update":
|
|
1568
|
+
return this.#mapDelta(event);
|
|
1569
|
+
case "message_end":
|
|
1570
|
+
return this.#mapMessage(event);
|
|
1571
|
+
case "tool_execution_start": {
|
|
1572
|
+
const tool = this.#summary(event.toolCallId, event.toolName, event.args);
|
|
1573
|
+
this.#openTools.set(event.toolCallId, tool);
|
|
1574
|
+
return [{ type: "tool_start", turnId: this.#turnId, tool }];
|
|
1575
|
+
}
|
|
1576
|
+
case "tool_execution_update":
|
|
1577
|
+
return [
|
|
1578
|
+
{
|
|
1579
|
+
type: "tool_progress",
|
|
1580
|
+
turnId: this.#turnId,
|
|
1581
|
+
toolCallId: event.toolCallId,
|
|
1582
|
+
text: codeAgentClip4(CodeAgentEventMapper2.#renderToolText(event.partialResult), codeAgentLabelChars)
|
|
1583
|
+
}
|
|
1584
|
+
];
|
|
1585
|
+
case "tool_execution_end":
|
|
1586
|
+
return [this.#mapToolEnd(event)];
|
|
1587
|
+
case "queue_update":
|
|
1588
|
+
return [{ type: "queue", steering: [...event.steering], followUp: [...event.followUp] }];
|
|
1589
|
+
case "compaction_start":
|
|
1590
|
+
return [{ type: "compaction", phase: "start", reason: event.reason }];
|
|
1591
|
+
case "compaction_end":
|
|
1592
|
+
return [{ type: "compaction", phase: "end", reason: event.reason }];
|
|
1593
|
+
case "auto_retry_start":
|
|
1594
|
+
return [
|
|
1595
|
+
{
|
|
1596
|
+
type: "retry",
|
|
1597
|
+
attempt: event.attempt,
|
|
1598
|
+
maxAttempts: event.maxAttempts,
|
|
1599
|
+
delayMs: event.delayMs,
|
|
1600
|
+
message: event.errorMessage
|
|
1601
|
+
}
|
|
1602
|
+
];
|
|
1603
|
+
default:
|
|
1604
|
+
return [];
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
markBlocked(toolCallId, toolName, args, reason) {
|
|
1608
|
+
this.#blocked.set(toolCallId, reason);
|
|
1609
|
+
if (this.#openTools.has(toolCallId))
|
|
1610
|
+
return [];
|
|
1611
|
+
const tool = this.#summary(toolCallId, toolName, args);
|
|
1612
|
+
this.#openTools.set(toolCallId, tool);
|
|
1613
|
+
return [{ type: "tool_start", turnId: this.#turnId, tool }];
|
|
1614
|
+
}
|
|
1615
|
+
flushBlocked() {
|
|
1616
|
+
const frames = [];
|
|
1617
|
+
for (const [toolCallId, reason] of this.#blocked) {
|
|
1618
|
+
const tool = this.#openTools.get(toolCallId);
|
|
1619
|
+
if (!tool)
|
|
1620
|
+
continue;
|
|
1621
|
+
this.#openTools.delete(toolCallId);
|
|
1622
|
+
frames.push({
|
|
1623
|
+
type: "tool_end",
|
|
1624
|
+
turnId: this.#turnId,
|
|
1625
|
+
tool,
|
|
1626
|
+
outcome: "blocked",
|
|
1627
|
+
output: reason,
|
|
1628
|
+
truncated: false
|
|
1629
|
+
});
|
|
1630
|
+
}
|
|
1631
|
+
this.#blocked.clear();
|
|
1632
|
+
return frames;
|
|
1633
|
+
}
|
|
1634
|
+
#mapToolEnd(event) {
|
|
1635
|
+
const tool = this.#openTools.get(event.toolCallId) ?? this.#summary(event.toolCallId, event.toolName, undefined);
|
|
1636
|
+
this.#openTools.delete(event.toolCallId);
|
|
1637
|
+
const reason = this.#blocked.get(event.toolCallId);
|
|
1638
|
+
this.#blocked.delete(event.toolCallId);
|
|
1639
|
+
const full = reason ?? CodeAgentEventMapper2.#renderToolText(event.result);
|
|
1640
|
+
return {
|
|
1641
|
+
type: "tool_end",
|
|
1642
|
+
turnId: this.#turnId,
|
|
1643
|
+
tool,
|
|
1644
|
+
outcome: reason ? "blocked" : event.isError ? "error" : "ok",
|
|
1645
|
+
output: codeAgentClip4(full, codeAgentOutputChars4),
|
|
1646
|
+
truncated: full.length > codeAgentOutputChars4
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
noteOutcome(reason) {
|
|
1650
|
+
this.#outcome = reason;
|
|
1651
|
+
}
|
|
1652
|
+
#stopReason(event) {
|
|
1653
|
+
const noted = this.#outcome;
|
|
1654
|
+
this.#outcome = undefined;
|
|
1655
|
+
if (noted)
|
|
1656
|
+
return noted;
|
|
1657
|
+
if (event.willRetry)
|
|
1658
|
+
return "error";
|
|
1659
|
+
const last = event.messages.at(-1);
|
|
1660
|
+
if (last?.stopReason === "aborted")
|
|
1661
|
+
return "aborted";
|
|
1662
|
+
if (last?.stopReason === "error")
|
|
1663
|
+
return "error";
|
|
1664
|
+
if (last?.stopReason === "length")
|
|
1665
|
+
return "truncated";
|
|
1666
|
+
return "done";
|
|
1667
|
+
}
|
|
1668
|
+
#summary(toolCallId, name, args) {
|
|
1669
|
+
const path = CodeAgentEventMapper2.#pathOf(args);
|
|
1670
|
+
return {
|
|
1671
|
+
toolCallId,
|
|
1672
|
+
name,
|
|
1673
|
+
op: toolOps[name] ?? "other",
|
|
1674
|
+
...path ? { path } : {},
|
|
1675
|
+
title: codeAgentClip4(`${name}${CodeAgentEventMapper2.#renderArgs(args)}`, codeAgentLabelChars)
|
|
1676
|
+
};
|
|
1677
|
+
}
|
|
1678
|
+
#mapDelta(event) {
|
|
1679
|
+
const inner = event.assistantMessageEvent;
|
|
1680
|
+
if (inner.type === "text_delta")
|
|
1681
|
+
return [{ type: "text_delta", turnId: this.#turnId, text: inner.delta }];
|
|
1682
|
+
if (inner.type === "thinking_delta")
|
|
1683
|
+
return [{ type: "thinking_delta", turnId: this.#turnId, text: inner.delta }];
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
#mapMessage(event) {
|
|
1687
|
+
const message = event.message;
|
|
1688
|
+
if (message.role !== "assistant" && message.role !== "user")
|
|
1689
|
+
return [];
|
|
1690
|
+
const text = CodeAgentEventMapper2.#renderContent(message.content);
|
|
1691
|
+
if (!text)
|
|
1692
|
+
return [];
|
|
1693
|
+
return [{ type: "message", turnId: this.#turnId, role: message.role, text }];
|
|
1694
|
+
}
|
|
1695
|
+
static #pathOf(args) {
|
|
1696
|
+
if (!args || typeof args !== "object")
|
|
1697
|
+
return;
|
|
1698
|
+
const value = args.path;
|
|
1699
|
+
return typeof value === "string" && value ? value : undefined;
|
|
1700
|
+
}
|
|
1701
|
+
static #renderArgs(args) {
|
|
1702
|
+
if (!args || typeof args !== "object")
|
|
1703
|
+
return "";
|
|
1704
|
+
const entries = Object.entries(args).filter(([, value]) => value !== undefined);
|
|
1705
|
+
if (!entries.length)
|
|
1706
|
+
return "";
|
|
1707
|
+
const rendered = entries.map(([key, value]) => `${key}=${CodeAgentEventMapper2.#renderArgValue(value)}`).join(" ");
|
|
1708
|
+
return `(${codeAgentClip4(rendered, 120)})`;
|
|
1709
|
+
}
|
|
1710
|
+
static #renderArgValue(value) {
|
|
1711
|
+
if (Array.isArray(value))
|
|
1712
|
+
return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
1713
|
+
if (value && typeof value === "object") {
|
|
1714
|
+
const keys = Object.keys(value).length;
|
|
1715
|
+
return `{${keys} key${keys === 1 ? "" : "s"}}`;
|
|
1716
|
+
}
|
|
1717
|
+
return codeAgentClip4(String(value), 60);
|
|
1718
|
+
}
|
|
1719
|
+
static #renderContent(content) {
|
|
1720
|
+
if (typeof content === "string")
|
|
1721
|
+
return content;
|
|
1722
|
+
if (!Array.isArray(content))
|
|
1723
|
+
return "";
|
|
1724
|
+
return content.filter((part) => !!part && typeof part === "object" && part.type === "text").map((part) => part.text).join("");
|
|
1725
|
+
}
|
|
1726
|
+
static #renderToolText(value) {
|
|
1727
|
+
if (typeof value === "string")
|
|
1728
|
+
return value;
|
|
1729
|
+
if (value && typeof value === "object" && "content" in value)
|
|
1730
|
+
return CodeAgentEventMapper2.#renderContent(value.content);
|
|
1731
|
+
return value === undefined ? "" : JSON.stringify(value);
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentGate.ts
|
|
1736
|
+
import path8 from "path";
|
|
1737
|
+
var writeTools = new Set(["write", "edit"]);
|
|
1738
|
+
var commandTools = new Set(["bash"]);
|
|
1739
|
+
|
|
1740
|
+
class CodeAgentGate2 {
|
|
1741
|
+
#profile;
|
|
1742
|
+
#deny;
|
|
1743
|
+
#allow;
|
|
1744
|
+
#deniedFragments;
|
|
1745
|
+
constructor(profile) {
|
|
1746
|
+
this.#profile = profile;
|
|
1747
|
+
this.#deny = (profile.paths.deny ?? []).map((pattern) => new Bun.Glob(pattern));
|
|
1748
|
+
this.#allow = (profile.paths.allow ?? []).map((pattern) => new Bun.Glob(pattern));
|
|
1749
|
+
this.#deniedFragments = [...new Set((profile.paths.deny ?? []).map(CodeAgentGate2.#fragmentOf).filter(Boolean))];
|
|
1750
|
+
}
|
|
1751
|
+
verdict(toolName, args) {
|
|
1752
|
+
const target = CodeAgentGate2.#pathOf(args);
|
|
1753
|
+
if (target) {
|
|
1754
|
+
const denied = this.#deniedReason(target);
|
|
1755
|
+
if (denied)
|
|
1756
|
+
return { block: denied };
|
|
1757
|
+
}
|
|
1758
|
+
const command = CodeAgentGate2.#commandOf(args);
|
|
1759
|
+
if (command) {
|
|
1760
|
+
const denied = this.#deniedCommandReason(command);
|
|
1761
|
+
if (denied)
|
|
1762
|
+
return { block: denied };
|
|
1763
|
+
}
|
|
1764
|
+
if (!this.#needsApproval(toolName))
|
|
1765
|
+
return {};
|
|
1766
|
+
return { approval: CodeAgentGate2.#summarize(toolName, args) };
|
|
1767
|
+
}
|
|
1768
|
+
#deniedCommandReason(command) {
|
|
1769
|
+
const hit = this.#deniedFragments.find((fragment) => command.includes(fragment));
|
|
1770
|
+
if (!hit)
|
|
1771
|
+
return;
|
|
1772
|
+
return `The command names "${hit}", which the profile's deny list covers. Ask the user for the value instead of reading the file.`;
|
|
1773
|
+
}
|
|
1774
|
+
#deniedReason(target) {
|
|
1775
|
+
const absolute = path8.isAbsolute(target) ? target : path8.resolve(this.#profile.paths.root, target);
|
|
1776
|
+
const relative = path8.relative(this.#profile.paths.root, absolute);
|
|
1777
|
+
if (relative.startsWith("..") || path8.isAbsolute(relative))
|
|
1778
|
+
return `Path is outside the agent root (${this.#profile.paths.root}).`;
|
|
1779
|
+
if (this.#deny.some((glob) => glob.match(absolute) || glob.match(relative)))
|
|
1780
|
+
return "Path is on the profile's deny list (secrets and environment files are never readable).";
|
|
1781
|
+
if (this.#allow.length && !this.#allow.some((glob) => glob.match(absolute) || glob.match(relative)))
|
|
1782
|
+
return "Path is outside the profile's allow list.";
|
|
1783
|
+
return;
|
|
1784
|
+
}
|
|
1785
|
+
#needsApproval(toolName) {
|
|
1786
|
+
switch (this.#profile.approval) {
|
|
1787
|
+
case "never":
|
|
1788
|
+
return false;
|
|
1789
|
+
case "writes":
|
|
1790
|
+
return writeTools.has(toolName);
|
|
1791
|
+
case "commands":
|
|
1792
|
+
return commandTools.has(toolName);
|
|
1793
|
+
case "all":
|
|
1794
|
+
return true;
|
|
1795
|
+
default:
|
|
1796
|
+
return false;
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
static #fragmentOf(pattern) {
|
|
1800
|
+
const trimmed = pattern.replace(/^\*\*\//, "").replace(/\/\*\*$/, "/").replace(/^\*+/, "");
|
|
1801
|
+
return trimmed.includes("*") ? "" : trimmed;
|
|
1802
|
+
}
|
|
1803
|
+
static #commandOf(args) {
|
|
1804
|
+
if (!args || typeof args !== "object")
|
|
1805
|
+
return;
|
|
1806
|
+
const value = args.command;
|
|
1807
|
+
return typeof value === "string" && value ? value : undefined;
|
|
1808
|
+
}
|
|
1809
|
+
static #pathOf(args) {
|
|
1810
|
+
if (!args || typeof args !== "object")
|
|
1811
|
+
return;
|
|
1812
|
+
const value = args.path;
|
|
1813
|
+
return typeof value === "string" && value ? value : undefined;
|
|
1814
|
+
}
|
|
1815
|
+
static #summarize(toolName, args) {
|
|
1816
|
+
const record = args ?? {};
|
|
1817
|
+
if (toolName === "bash")
|
|
1818
|
+
return `run: ${String(record.command ?? "").slice(0, 200)}`;
|
|
1819
|
+
const target = CodeAgentGate2.#pathOf(args);
|
|
1820
|
+
return target ? `${toolName} ${target}` : toolName;
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentUi.ts
|
|
1825
|
+
class CodeAgentUi2 {
|
|
1826
|
+
#handlers;
|
|
1827
|
+
constructor(handlers) {
|
|
1828
|
+
this.#handlers = handlers;
|
|
1829
|
+
}
|
|
1830
|
+
context() {
|
|
1831
|
+
const noop = () => {};
|
|
1832
|
+
const ui = {
|
|
1833
|
+
select: async (title, options) => await this.#handlers.ask(title, "select", options),
|
|
1834
|
+
confirm: async (title, message) => {
|
|
1835
|
+
const answer = await this.#handlers.ask(message ? `${title}: ${message}` : title, "confirm");
|
|
1836
|
+
return answer === "yes" || answer === "true";
|
|
1837
|
+
},
|
|
1838
|
+
input: async (title) => await this.#handlers.ask(title, "text"),
|
|
1839
|
+
notify: (message, type = "info") => this.#handlers.notify(type, message),
|
|
1840
|
+
editor: async (title, prefill) => await this.#handlers.ask(title, "text") ?? prefill,
|
|
1841
|
+
onTerminalInput: () => noop,
|
|
1842
|
+
setStatus: noop,
|
|
1843
|
+
setWorkingMessage: noop,
|
|
1844
|
+
setWorkingVisible: noop,
|
|
1845
|
+
setWorkingIndicator: noop,
|
|
1846
|
+
setHiddenThinkingLabel: noop,
|
|
1847
|
+
setWidget: noop,
|
|
1848
|
+
setFooter: noop,
|
|
1849
|
+
setHeader: noop,
|
|
1850
|
+
setTitle: noop,
|
|
1851
|
+
custom: async () => {
|
|
1852
|
+
return;
|
|
1853
|
+
},
|
|
1854
|
+
pasteToEditor: noop,
|
|
1855
|
+
setEditorText: noop,
|
|
1856
|
+
getEditorText: () => "",
|
|
1857
|
+
addAutocompleteProvider: noop,
|
|
1858
|
+
setCustomEditorComponent: noop
|
|
1859
|
+
};
|
|
1860
|
+
return ui;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeMailbox.ts
|
|
1865
|
+
import {
|
|
1866
|
+
appendFileSync,
|
|
1867
|
+
existsSync as existsSync10,
|
|
1868
|
+
mkdirSync as mkdirSync3,
|
|
1869
|
+
readdirSync as readdirSync2,
|
|
1870
|
+
readFileSync as readFileSync5,
|
|
1871
|
+
rmSync,
|
|
1872
|
+
statSync as statSync3,
|
|
1873
|
+
writeFileSync as writeFileSync2
|
|
1874
|
+
} from "fs";
|
|
1875
|
+
import path9 from "path";
|
|
1876
|
+
|
|
1877
|
+
class CodeMailbox2 {
|
|
1878
|
+
static staleMs = 20000;
|
|
1879
|
+
static beatMs = 5000;
|
|
1880
|
+
static pollMs = 1500;
|
|
1881
|
+
static sendsPerMinute = 20;
|
|
1882
|
+
#dir;
|
|
1883
|
+
#id;
|
|
1884
|
+
#name;
|
|
1885
|
+
#read = 0;
|
|
1886
|
+
#sent = [];
|
|
1887
|
+
#beat = null;
|
|
1888
|
+
#poll = null;
|
|
1889
|
+
constructor(dir, id, name) {
|
|
1890
|
+
this.#dir = dir;
|
|
1891
|
+
this.#id = id;
|
|
1892
|
+
this.#name = name;
|
|
1893
|
+
}
|
|
1894
|
+
get inbox() {
|
|
1895
|
+
return path9.join(this.#dir, "inbox", `${this.#id}.jsonl`);
|
|
1896
|
+
}
|
|
1897
|
+
open(cwd, onMail) {
|
|
1898
|
+
mkdirSync3(path9.join(this.#dir, "live"), { recursive: true });
|
|
1899
|
+
mkdirSync3(path9.join(this.#dir, "inbox"), { recursive: true });
|
|
1900
|
+
this.#read = CodeMailbox2.#lines(this.inbox).length;
|
|
1901
|
+
this.beat(cwd);
|
|
1902
|
+
this.#beat = setInterval(() => this.beat(cwd), CodeMailbox2.beatMs);
|
|
1903
|
+
this.#poll = setInterval(() => {
|
|
1904
|
+
for (const mail of this.#drain())
|
|
1905
|
+
onMail(mail);
|
|
1906
|
+
}, CodeMailbox2.pollMs);
|
|
1907
|
+
}
|
|
1908
|
+
close() {
|
|
1909
|
+
if (this.#beat)
|
|
1910
|
+
clearInterval(this.#beat);
|
|
1911
|
+
if (this.#poll)
|
|
1912
|
+
clearInterval(this.#poll);
|
|
1913
|
+
this.#beat = null;
|
|
1914
|
+
this.#poll = null;
|
|
1915
|
+
try {
|
|
1916
|
+
rmSync(path9.join(this.#dir, "live", `${this.#id}.json`), { force: true });
|
|
1917
|
+
} catch {}
|
|
1918
|
+
}
|
|
1919
|
+
rename(name, cwd) {
|
|
1920
|
+
this.#name = name;
|
|
1921
|
+
this.beat(cwd);
|
|
1922
|
+
}
|
|
1923
|
+
beat(cwd) {
|
|
1924
|
+
const peer = { id: this.#id, name: this.#name, cwd, pid: process.pid, at: Date.now() };
|
|
1925
|
+
try {
|
|
1926
|
+
writeFileSync2(path9.join(this.#dir, "live", `${this.#id}.json`), JSON.stringify(peer));
|
|
1927
|
+
} catch {}
|
|
1928
|
+
}
|
|
1929
|
+
peers() {
|
|
1930
|
+
const dir = path9.join(this.#dir, "live");
|
|
1931
|
+
if (!existsSync10(dir))
|
|
1932
|
+
return [];
|
|
1933
|
+
const now = Date.now();
|
|
1934
|
+
return readdirSync2(dir).filter((entry) => entry.endsWith(".json")).map((entry) => CodeMailbox2.#peer(path9.join(dir, entry))).filter((peer) => !!peer && peer.id !== this.#id && now - peer.at < CodeMailbox2.staleMs).sort((left, right) => right.at - left.at);
|
|
1935
|
+
}
|
|
1936
|
+
find(target) {
|
|
1937
|
+
const needle = target.trim().toLowerCase();
|
|
1938
|
+
const peers = this.peers();
|
|
1939
|
+
const byName = peers.filter((peer) => peer.name.toLowerCase() === needle);
|
|
1940
|
+
if (byName.length === 1)
|
|
1941
|
+
return byName[0];
|
|
1942
|
+
const byPrefix = peers.filter((peer) => peer.id.startsWith(needle) || peer.name.toLowerCase().startsWith(needle));
|
|
1943
|
+
return byPrefix.length === 1 ? byPrefix[0] : undefined;
|
|
1944
|
+
}
|
|
1945
|
+
send(to, text) {
|
|
1946
|
+
const now = Date.now();
|
|
1947
|
+
this.#sent = this.#sent.filter((at) => now - at < 60000);
|
|
1948
|
+
if (this.#sent.length >= CodeMailbox2.sendsPerMinute)
|
|
1949
|
+
throw new Error(`Too many messages: ${CodeMailbox2.sendsPerMinute} a minute is the ceiling.`);
|
|
1950
|
+
this.#sent.push(now);
|
|
1951
|
+
const mail = { at: now, from: this.#id, fromName: this.#name, text };
|
|
1952
|
+
mkdirSync3(path9.join(this.#dir, "inbox"), { recursive: true });
|
|
1953
|
+
appendFileSync(path9.join(this.#dir, "inbox", `${to}.jsonl`), `${JSON.stringify(mail)}
|
|
1954
|
+
`);
|
|
1955
|
+
}
|
|
1956
|
+
#drain() {
|
|
1957
|
+
const lines = CodeMailbox2.#lines(this.inbox);
|
|
1958
|
+
if (lines.length <= this.#read)
|
|
1959
|
+
return [];
|
|
1960
|
+
const fresh = lines.slice(this.#read);
|
|
1961
|
+
this.#read = lines.length;
|
|
1962
|
+
return fresh.map((line) => CodeMailbox2.#parse(line)).filter((mail) => !!mail?.text && typeof mail.from === "string");
|
|
1963
|
+
}
|
|
1964
|
+
static #peer(file) {
|
|
1965
|
+
try {
|
|
1966
|
+
const peer = JSON.parse(readFileSync5(file, "utf8"));
|
|
1967
|
+
return typeof peer.id === "string" ? { ...peer, at: peer.at || statSync3(file).mtimeMs } : undefined;
|
|
1968
|
+
} catch {
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
}
|
|
1972
|
+
static #lines(file) {
|
|
1973
|
+
try {
|
|
1974
|
+
return readFileSync5(file, "utf8").split(`
|
|
1975
|
+
`).filter((line) => !!line.trim());
|
|
1976
|
+
} catch {
|
|
1977
|
+
return [];
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
static #parse(line) {
|
|
1981
|
+
try {
|
|
1982
|
+
return JSON.parse(line);
|
|
1983
|
+
} catch {
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeSessionFork.ts
|
|
1990
|
+
import { randomBytes } from "crypto";
|
|
1991
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
1992
|
+
import path10 from "path";
|
|
1993
|
+
import { codeAgentSessionName } from "akanjs/common";
|
|
1994
|
+
class CodeSessionFork2 {
|
|
1995
|
+
static fork(dir, id, name) {
|
|
1996
|
+
const source = CodeSessionIndex2.fileOf(dir, id);
|
|
1997
|
+
if (!source)
|
|
1998
|
+
throw new Error(`No stored session ${id} in ${dir}`);
|
|
1999
|
+
const lines = readFileSync6(source, "utf8").split(`
|
|
2000
|
+
`).filter((line) => !!line.trim());
|
|
2001
|
+
if (!lines.length)
|
|
2002
|
+
throw new Error(`Session ${id} has nothing in it to fork`);
|
|
2003
|
+
const at = new Date;
|
|
2004
|
+
const forked = Bun.randomUUIDv7();
|
|
2005
|
+
const records = lines.map((line) => CodeSessionFork2.#parse(line)).filter((record) => !!record);
|
|
2006
|
+
const first = records.find((record) => record.type === "session");
|
|
2007
|
+
if (!first)
|
|
2008
|
+
throw new Error(`Session ${id} carries no session record`);
|
|
2009
|
+
first.id = forked;
|
|
2010
|
+
first.timestamp = at.toISOString();
|
|
2011
|
+
records.push(CodeSessionFork2.#named(records, name ?? CodeSessionFork2.#nameOf(records), at));
|
|
2012
|
+
const file = path10.join(dir, `${at.toISOString().replace(/[:.]/g, "-")}_${forked}.jsonl`);
|
|
2013
|
+
writeFileSync3(file, `${records.map((record) => JSON.stringify(record)).join(`
|
|
2014
|
+
`)}
|
|
2015
|
+
`);
|
|
2016
|
+
return { id: forked, file };
|
|
2017
|
+
}
|
|
2018
|
+
static #named(records, name, at) {
|
|
2019
|
+
const parent = records.at(-1)?.id;
|
|
2020
|
+
return {
|
|
2021
|
+
type: "session_info",
|
|
2022
|
+
id: randomBytes(4).toString("hex"),
|
|
2023
|
+
parentId: typeof parent === "string" ? parent : null,
|
|
2024
|
+
timestamp: at.toISOString(),
|
|
2025
|
+
name
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
static #nameOf(records) {
|
|
2029
|
+
const named = records.filter((record) => record.type === "session_info").at(-1);
|
|
2030
|
+
const current = typeof named?.name === "string" ? named.name : "";
|
|
2031
|
+
return codeAgentSessionName(current ? `${current} fork` : "fork");
|
|
2032
|
+
}
|
|
2033
|
+
static #parse(line) {
|
|
2034
|
+
try {
|
|
2035
|
+
return JSON.parse(line);
|
|
2036
|
+
} catch {
|
|
2037
|
+
return;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgent.ts
|
|
2043
|
+
class CodeAgent2 {
|
|
2044
|
+
#profile;
|
|
2045
|
+
#gate;
|
|
2046
|
+
#mapper = new CodeAgentEventMapper2;
|
|
2047
|
+
#asks = new CodeAgentAsks2;
|
|
2048
|
+
#listeners = new Set;
|
|
2049
|
+
#session;
|
|
2050
|
+
#disposePlugins;
|
|
2051
|
+
#seq = 0;
|
|
2052
|
+
#disposed = false;
|
|
2053
|
+
#sessionDir;
|
|
2054
|
+
#mailbox;
|
|
2055
|
+
#replay = [];
|
|
2056
|
+
#replayFrom = 0;
|
|
2057
|
+
#lastContextTokens;
|
|
2058
|
+
constructor(profile) {
|
|
2059
|
+
this.#profile = profile;
|
|
2060
|
+
this.#gate = new CodeAgentGate2(profile);
|
|
2061
|
+
}
|
|
2062
|
+
static async create(options) {
|
|
2063
|
+
const workspaceRoot = options.workspace.workspaceRoot;
|
|
2064
|
+
const cwd = options.cwd ?? workspaceRoot;
|
|
2065
|
+
const agent = new CodeAgent2(options.profile);
|
|
2066
|
+
const settingsManager = AkanCodeServices2.settings();
|
|
2067
|
+
const authStorage = AkanCodeServices2.auth(workspaceRoot);
|
|
2068
|
+
const modelRegistry = AkanCodeServices2.models(authStorage);
|
|
2069
|
+
const model = await akanCodeModel2(modelRegistry, options.model);
|
|
2070
|
+
const compaction = DEFAULT_COMPACTION_SETTINGS;
|
|
2071
|
+
const modelWarnings = akanCodeModelWarnings2(model, compaction.reserveTokens + compaction.keepRecentTokens);
|
|
2072
|
+
const akan = await AkanCodePlugins2.build({
|
|
2073
|
+
workspace: options.workspace,
|
|
2074
|
+
cwd,
|
|
2075
|
+
profile: options.profile,
|
|
2076
|
+
apps: options.apps ?? [],
|
|
2077
|
+
canSeeImages: akanCodeModelSupportsImages2(model),
|
|
2078
|
+
depth: options.depth ?? 0,
|
|
2079
|
+
currentSessionId: () => agent.sessionId,
|
|
2080
|
+
mailbox: () => agent.#mailbox,
|
|
2081
|
+
onNotice: (message) => agent.#emit({ type: "notice", level: "warning", message })
|
|
2082
|
+
});
|
|
2083
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
2084
|
+
cwd,
|
|
2085
|
+
agentDir: akanCodePaths2.globalDir(),
|
|
2086
|
+
settingsManager,
|
|
2087
|
+
...AkanCodeServices2.resourceOptions({
|
|
2088
|
+
workspaceRoot,
|
|
2089
|
+
cwd,
|
|
2090
|
+
profile: options.profile,
|
|
2091
|
+
extensions: [agent.#gateExtension(), ...akan.extensions, ...options.extensions ?? []]
|
|
2092
|
+
})
|
|
2093
|
+
});
|
|
2094
|
+
await resourceLoader.reload();
|
|
2095
|
+
const { session } = await createAgentSession({
|
|
2096
|
+
cwd,
|
|
2097
|
+
agentDir: akanCodePaths2.globalDir(),
|
|
2098
|
+
authStorage,
|
|
2099
|
+
modelRegistry,
|
|
2100
|
+
settingsManager,
|
|
2101
|
+
resourceLoader,
|
|
2102
|
+
model,
|
|
2103
|
+
sessionManager: AkanCodeServices2.sessions(workspaceRoot, cwd, options.profile, options.resume),
|
|
2104
|
+
noTools: "builtin",
|
|
2105
|
+
tools: [...options.profile.tools.builtin, ...akan.toolNames],
|
|
2106
|
+
customTools: options.customTools
|
|
2107
|
+
});
|
|
2108
|
+
agent.#disposePlugins = akan.dispose;
|
|
2109
|
+
if (options.profile.session.store === "file")
|
|
2110
|
+
agent.#sessionDir = akanCodePaths2.sessionsDir(workspaceRoot);
|
|
2111
|
+
await agent.#attach(session, options.mode ?? "print");
|
|
2112
|
+
if (agent.#sessionDir)
|
|
2113
|
+
agent.#openMail(akanCodePaths2.mailDir(workspaceRoot), cwd);
|
|
2114
|
+
for (const message of modelWarnings)
|
|
2115
|
+
agent.#emit({ type: "notice", level: "warning", message });
|
|
2116
|
+
return agent;
|
|
2117
|
+
}
|
|
2118
|
+
get profile() {
|
|
2119
|
+
return this.#profile;
|
|
2120
|
+
}
|
|
2121
|
+
get canSeeImages() {
|
|
2122
|
+
const model = this.#session?.model;
|
|
2123
|
+
return model ? akanCodeModelSupportsImages2(model) : false;
|
|
2124
|
+
}
|
|
2125
|
+
sessions() {
|
|
2126
|
+
return this.#sessionDir ? CodeSessionIndex2.list(this.#sessionDir) : [];
|
|
2127
|
+
}
|
|
2128
|
+
peers() {
|
|
2129
|
+
return this.#mailbox?.peers() ?? [];
|
|
2130
|
+
}
|
|
2131
|
+
send(target, text) {
|
|
2132
|
+
const mailbox = this.#mailbox;
|
|
2133
|
+
if (!mailbox)
|
|
2134
|
+
throw new Error("This profile keeps no shared mailbox, so it has no peers.");
|
|
2135
|
+
const peer = mailbox.find(target);
|
|
2136
|
+
if (!peer)
|
|
2137
|
+
throw new Error(`No live session matches "${target}". Ask for the list first.`);
|
|
2138
|
+
mailbox.send(peer.id, text);
|
|
2139
|
+
return peer;
|
|
2140
|
+
}
|
|
2141
|
+
#openMail(dir, cwd) {
|
|
2142
|
+
const session = this.#session;
|
|
2143
|
+
if (!session)
|
|
2144
|
+
return;
|
|
2145
|
+
this.#mailbox = new CodeMailbox2(dir, session.sessionId, session.sessionName ?? session.sessionId.slice(0, 8));
|
|
2146
|
+
this.#mailbox.open(cwd, (mail) => {
|
|
2147
|
+
const from = `${mail.fromName} (${mail.from.slice(0, 8)})`;
|
|
2148
|
+
this.#emit({ type: "notice", level: "info", message: `message from ${from}` });
|
|
2149
|
+
this.prompt(`Message from another session \u2014 ${from}:
|
|
2150
|
+
${mail.text}`).catch((error) => this.#emit({ type: "notice", level: "error", message: String(error) }));
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
get sessionId() {
|
|
2154
|
+
return this.#session?.sessionId ?? "";
|
|
2155
|
+
}
|
|
2156
|
+
get isStreaming() {
|
|
2157
|
+
return this.#session?.isStreaming ?? false;
|
|
2158
|
+
}
|
|
2159
|
+
get tokensUsed() {
|
|
2160
|
+
return this.#session?.getSessionStats().tokens.total ?? 0;
|
|
2161
|
+
}
|
|
2162
|
+
get info() {
|
|
2163
|
+
const session = this.#require();
|
|
2164
|
+
const model = session.model;
|
|
2165
|
+
return {
|
|
2166
|
+
sessionId: session.sessionId,
|
|
2167
|
+
cwd: this.#profile.paths.root,
|
|
2168
|
+
profile: this.#profile.name,
|
|
2169
|
+
model: model ? { provider: model.provider, id: model.id, name: model.name } : undefined,
|
|
2170
|
+
tools: session.getActiveToolNames(),
|
|
2171
|
+
contextTokens: model?.contextWindow,
|
|
2172
|
+
effort: session.supportsThinking() ? session.thinkingLevel : undefined,
|
|
2173
|
+
name: session.sessionName,
|
|
2174
|
+
interaction: this.#profile.interaction
|
|
2175
|
+
};
|
|
2176
|
+
}
|
|
2177
|
+
announce() {
|
|
2178
|
+
this.#emit({ type: "session", info: this.info });
|
|
2179
|
+
this.#restore();
|
|
2180
|
+
}
|
|
2181
|
+
#restore() {
|
|
2182
|
+
const session = this.#session;
|
|
2183
|
+
if (!session)
|
|
2184
|
+
return;
|
|
2185
|
+
const turnId = `restored:${session.sessionId}`;
|
|
2186
|
+
for (const message of session.messages) {
|
|
2187
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
2188
|
+
continue;
|
|
2189
|
+
const text = CodeAgent2.#textOf(message.content);
|
|
2190
|
+
if (text)
|
|
2191
|
+
this.#emit({ type: "message", turnId, role: message.role, text });
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
static #textOf(content) {
|
|
2195
|
+
if (typeof content === "string")
|
|
2196
|
+
return content.trim();
|
|
2197
|
+
if (!Array.isArray(content))
|
|
2198
|
+
return "";
|
|
2199
|
+
return content.map((part) => typeof part === "object" && part && ("text" in part) ? String(part.text) : "").join("").trim();
|
|
2200
|
+
}
|
|
2201
|
+
on(listener) {
|
|
2202
|
+
this.#listeners.add(listener);
|
|
2203
|
+
return () => this.#listeners.delete(listener);
|
|
2204
|
+
}
|
|
2205
|
+
static replayLimit = 2000;
|
|
2206
|
+
async state({ sinceSeq } = {}) {
|
|
2207
|
+
const base = { info: this.info, streaming: this.isStreaming, replayFrom: this.#replayFrom };
|
|
2208
|
+
if (sinceSeq === undefined)
|
|
2209
|
+
return base;
|
|
2210
|
+
return { ...base, frames: this.#replay.filter((event) => event.seq > sinceSeq) };
|
|
2211
|
+
}
|
|
2212
|
+
async prompt(message, images) {
|
|
2213
|
+
const session = this.#require();
|
|
2214
|
+
if (!session.sessionName && message.trim())
|
|
2215
|
+
this.setName(codeAgentSessionName2(message));
|
|
2216
|
+
const attachments = await CodeAgent2.#attachments(images);
|
|
2217
|
+
const options = attachments.length ? { images: attachments } : {};
|
|
2218
|
+
if (session.isStreaming)
|
|
2219
|
+
return await session.prompt(message, { ...options, streamingBehavior: "followUp" });
|
|
2220
|
+
await session.prompt(message, { ...options, source: "extension" });
|
|
2221
|
+
}
|
|
2222
|
+
async abort() {
|
|
2223
|
+
this.#mapper.noteOutcome("aborted");
|
|
2224
|
+
this.#asks.clear();
|
|
2225
|
+
await this.#require().abort();
|
|
2226
|
+
}
|
|
2227
|
+
async answer(questionId, answer) {
|
|
2228
|
+
const question = this.#asks.questionOf(questionId);
|
|
2229
|
+
const rendered = question ? codeAgentRenderAnswer(question, answer) : answer.text ?? "";
|
|
2230
|
+
if (this.#asks.answer(questionId, rendered)) {
|
|
2231
|
+
this.#emit({ type: "question_resolved", questionId, answer, rendered });
|
|
2232
|
+
return true;
|
|
2233
|
+
}
|
|
2234
|
+
if (this.#profile.interaction.question !== "suspend")
|
|
2235
|
+
return false;
|
|
2236
|
+
this.#emit({ type: "question_resolved", questionId, answer, rendered });
|
|
2237
|
+
await this.prompt(`The user answered: ${rendered}
|
|
2238
|
+
Continue the task.`);
|
|
2239
|
+
return true;
|
|
2240
|
+
}
|
|
2241
|
+
async approve(approvalId, approved) {
|
|
2242
|
+
if (this.#asks.resolveApproval(approvalId, approved)) {
|
|
2243
|
+
this.#emit({ type: "approval_resolved", approvalId, approved });
|
|
2244
|
+
return true;
|
|
2245
|
+
}
|
|
2246
|
+
if (this.#profile.interaction.approval !== "suspend")
|
|
2247
|
+
return false;
|
|
2248
|
+
this.#emit({ type: "approval_resolved", approvalId, approved });
|
|
2249
|
+
if (approved)
|
|
2250
|
+
await this.prompt("The user approved the pending action. Retry it and continue.");
|
|
2251
|
+
return true;
|
|
2252
|
+
}
|
|
2253
|
+
async compact(instructions) {
|
|
2254
|
+
await this.#require().compact(instructions);
|
|
2255
|
+
}
|
|
2256
|
+
setName(name) {
|
|
2257
|
+
const clean = name.trim();
|
|
2258
|
+
if (!clean)
|
|
2259
|
+
return;
|
|
2260
|
+
this.#require().setSessionName(clean);
|
|
2261
|
+
this.#mailbox?.rename(clean, this.#profile.paths.root);
|
|
2262
|
+
this.#emit({ type: "session", info: this.info });
|
|
2263
|
+
}
|
|
2264
|
+
fork(name) {
|
|
2265
|
+
if (!this.#sessionDir)
|
|
2266
|
+
throw new Error("This profile keeps no sessions on disk, so there is nothing to fork");
|
|
2267
|
+
return CodeSessionFork2.fork(this.#sessionDir, this.sessionId, name);
|
|
2268
|
+
}
|
|
2269
|
+
efforts() {
|
|
2270
|
+
const session = this.#session;
|
|
2271
|
+
if (!session?.supportsThinking())
|
|
2272
|
+
return [];
|
|
2273
|
+
return session.getAvailableThinkingLevels();
|
|
2274
|
+
}
|
|
2275
|
+
setEffort(effort) {
|
|
2276
|
+
const session = this.#require();
|
|
2277
|
+
if (!session.supportsThinking())
|
|
2278
|
+
throw new Error(`${session.model?.id ?? "this model"} does no reasoning`);
|
|
2279
|
+
session.setThinkingLevel(effort);
|
|
2280
|
+
this.#emit({ type: "session", info: this.info });
|
|
2281
|
+
}
|
|
2282
|
+
async setModel(ref) {
|
|
2283
|
+
const session = this.#require();
|
|
2284
|
+
const model = await akanCodeModel2(session.modelRegistry, ref);
|
|
2285
|
+
if (!model)
|
|
2286
|
+
throw new Error(`Unknown model: ${ref.provider}/${ref.id}`);
|
|
2287
|
+
await session.setModel(model);
|
|
2288
|
+
this.#emit({ type: "session", info: this.info });
|
|
2289
|
+
}
|
|
2290
|
+
async waitForIdle() {
|
|
2291
|
+
await this.#require().waitForIdle();
|
|
2292
|
+
}
|
|
2293
|
+
dispose() {
|
|
2294
|
+
this.#mailbox?.close();
|
|
2295
|
+
if (this.#disposed)
|
|
2296
|
+
return;
|
|
2297
|
+
this.#disposed = true;
|
|
2298
|
+
this.#asks.clear();
|
|
2299
|
+
this.#listeners.clear();
|
|
2300
|
+
this.#disposePlugins?.();
|
|
2301
|
+
this.#session?.dispose();
|
|
2302
|
+
}
|
|
2303
|
+
async#attach(session, mode) {
|
|
2304
|
+
this.#session = session;
|
|
2305
|
+
session.subscribe((event) => {
|
|
2306
|
+
for (const body of this.#mapper.map(event))
|
|
2307
|
+
this.#emit(body);
|
|
2308
|
+
this.#emitContextUsage(event.type);
|
|
2309
|
+
});
|
|
2310
|
+
await session.bindExtensions({
|
|
2311
|
+
mode,
|
|
2312
|
+
uiContext: this.#ui().context(),
|
|
2313
|
+
abortHandler: () => void session.abort(),
|
|
2314
|
+
onError: (error) => this.#emit({ type: "error", message: String(error), fatal: false })
|
|
2315
|
+
});
|
|
2316
|
+
}
|
|
2317
|
+
static async#attachments(images) {
|
|
2318
|
+
if (!images?.length)
|
|
2319
|
+
return [];
|
|
2320
|
+
return await Promise.all(images.map(async (image) => {
|
|
2321
|
+
if ("data" in image)
|
|
2322
|
+
return { type: "image", data: image.data, mimeType: image.mime };
|
|
2323
|
+
const file = Bun.file(image.path);
|
|
2324
|
+
return {
|
|
2325
|
+
type: "image",
|
|
2326
|
+
data: Buffer.from(await file.arrayBuffer()).toString("base64"),
|
|
2327
|
+
mimeType: file.type || "image/png"
|
|
2328
|
+
};
|
|
2329
|
+
}));
|
|
2330
|
+
}
|
|
2331
|
+
#emitContextUsage(eventType) {
|
|
2332
|
+
if (eventType !== "agent_end" && eventType !== "compaction_end")
|
|
2333
|
+
return;
|
|
2334
|
+
const usage = this.#session?.getContextUsage();
|
|
2335
|
+
if (usage?.tokens == null)
|
|
2336
|
+
return;
|
|
2337
|
+
if (usage.tokens === this.#lastContextTokens)
|
|
2338
|
+
return;
|
|
2339
|
+
this.#lastContextTokens = usage.tokens;
|
|
2340
|
+
this.#emit({ type: "context", used: usage.tokens, max: usage.contextWindow || undefined });
|
|
2341
|
+
}
|
|
2342
|
+
#require() {
|
|
2343
|
+
if (!this.#session)
|
|
2344
|
+
throw new Error("CodeAgent is not started");
|
|
2345
|
+
return this.#session;
|
|
2346
|
+
}
|
|
2347
|
+
#emit(body) {
|
|
2348
|
+
this.#seq += 1;
|
|
2349
|
+
const event = { ...body, seq: this.#seq };
|
|
2350
|
+
this.#remember(event);
|
|
2351
|
+
for (const listener of this.#listeners)
|
|
2352
|
+
listener(event);
|
|
2353
|
+
}
|
|
2354
|
+
#remember(event) {
|
|
2355
|
+
if (event.type === "idle") {
|
|
2356
|
+
this.#replay.length = 0;
|
|
2357
|
+
this.#replayFrom = event.seq;
|
|
2358
|
+
return;
|
|
2359
|
+
}
|
|
2360
|
+
this.#replay.push(event);
|
|
2361
|
+
while (this.#replay.length > CodeAgent2.replayLimit) {
|
|
2362
|
+
const dropped = this.#replay.shift();
|
|
2363
|
+
this.#replayFrom = dropped?.seq ?? this.#replayFrom;
|
|
2364
|
+
}
|
|
2365
|
+
}
|
|
2366
|
+
#gateExtension() {
|
|
2367
|
+
return {
|
|
2368
|
+
name: "akan-profile-gate",
|
|
2369
|
+
factory: (pi) => {
|
|
2370
|
+
pi.on("tool_call", async (event) => {
|
|
2371
|
+
const verdict = this.#gate.verdict(event.toolName, event.input);
|
|
2372
|
+
if (verdict.block)
|
|
2373
|
+
return this.#refuse(event.toolCallId, event.toolName, event.input, verdict.block);
|
|
2374
|
+
if (!verdict.approval)
|
|
2375
|
+
return;
|
|
2376
|
+
const approved = await this.#requestApproval(event.toolCallId, event.toolName, event.input, verdict.approval);
|
|
2377
|
+
if (approved)
|
|
2378
|
+
return;
|
|
2379
|
+
return this.#refuse(event.toolCallId, event.toolName, event.input, "The user declined this action.");
|
|
2380
|
+
});
|
|
2381
|
+
}
|
|
2382
|
+
};
|
|
2383
|
+
}
|
|
2384
|
+
#refuse(toolCallId, toolName, args, reason) {
|
|
2385
|
+
for (const body of this.#mapper.markBlocked(toolCallId, toolName, args, reason))
|
|
2386
|
+
this.#emit(body);
|
|
2387
|
+
return { block: true, reason };
|
|
2388
|
+
}
|
|
2389
|
+
async#requestApproval(toolCallId, name, args, summary) {
|
|
2390
|
+
if (!this.#profile.ui.canPrompt)
|
|
2391
|
+
return true;
|
|
2392
|
+
const approvalId = this.#asks.nextId("a");
|
|
2393
|
+
this.#emit({
|
|
2394
|
+
type: "approval",
|
|
2395
|
+
request: {
|
|
2396
|
+
approvalId,
|
|
2397
|
+
toolCallId,
|
|
2398
|
+
name,
|
|
2399
|
+
summary: codeAgentClip5(summary, codeAgentLabelChars2),
|
|
2400
|
+
policy: this.#profile.approval
|
|
2401
|
+
}
|
|
2402
|
+
});
|
|
2403
|
+
if (this.#profile.interaction.approval === "suspend") {
|
|
2404
|
+
this.#mapper.noteOutcome("awaiting");
|
|
2405
|
+
return false;
|
|
2406
|
+
}
|
|
2407
|
+
return await this.#asks.openApproval(approvalId);
|
|
2408
|
+
}
|
|
2409
|
+
#ui() {
|
|
2410
|
+
return new CodeAgentUi2({
|
|
2411
|
+
ask: async (prompt, kind, choices) => {
|
|
2412
|
+
if (!this.#profile.ui.canPrompt)
|
|
2413
|
+
return;
|
|
2414
|
+
const questionId = this.#asks.nextId("q");
|
|
2415
|
+
const question = {
|
|
2416
|
+
questionId,
|
|
2417
|
+
prompt,
|
|
2418
|
+
kind,
|
|
2419
|
+
...choices ? { options: choices.map((label) => ({ key: label, label })) } : {},
|
|
2420
|
+
freeText: kind === "text"
|
|
2421
|
+
};
|
|
2422
|
+
this.#emit({ type: "question", question });
|
|
2423
|
+
if (this.#profile.interaction.question === "suspend") {
|
|
2424
|
+
this.#mapper.noteOutcome("awaiting");
|
|
2425
|
+
return;
|
|
2426
|
+
}
|
|
2427
|
+
return await this.#asks.openQuestion(question) || undefined;
|
|
2428
|
+
},
|
|
2429
|
+
notify: (level, message) => this.#emit({ type: "notice", level, message })
|
|
2430
|
+
});
|
|
2431
|
+
}
|
|
2432
|
+
}
|
|
2433
|
+
|
|
2434
|
+
export { AkanEditScope2, DevLogFeedback2, PreviewChrome2, PreviewView2, PreviewFeedback2, TurnFeedback2, AkanCli2, AkanVerifier2, VerifyFeedback2, AkanToolPack2, MailToolPack2, akanCodePaths2, McpClient2, McpToolPack2, SessionToolPack2, WebToolPack2, AkanCodePlugins2, AkanEnvKeys2, AkanAuthStorageBackend2, akanSystemPrompt2, CodeSessionIndex2, AkanCodeServices2, CodeAgentAsks2, CodeAgentEventMapper2, CodeAgentGate2, CodeAgentUi2, CodeMailbox2, CodeSessionFork2, CodeAgent2 };
|