@akanjs/cli 3.0.0-beta.9 → 3.0.1
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-zfab7pkn.js +9 -0
- package/CodeAgentRpcHost-p7h6n2hp.js +7 -0
- package/CodeAgentRpcListener-7p7z5dd7.js +97 -0
- package/CodeTui-0zhnr0pw.js +1457 -0
- package/README.ko.md +83 -28
- package/README.md +70 -27
- package/TunnelShare-cwh9ezsm.js +9 -0
- package/{agent.command-0q17pnrs.js → agent.command-aja6z9mn.js} +8 -9
- package/akanCode.js +102 -0
- package/application.command-049ehgx3.js +246 -0
- package/{applicationBuildRunner-kxwbssgn.js → applicationBuildRunner-75n5rpwp.js} +121 -77
- package/buildBatch.proc.js +120 -32
- package/cloud.command-gw4kqxt8.js +95 -0
- package/cloud.script-c723cpcb.js +20 -0
- package/code.command-x9t6z5nw.js +154 -0
- package/commandManifest.json +1 -1
- package/consoleToStderr-h5bryj94.js +7 -0
- package/context.command-yfbe1rtd.js +48 -0
- package/context.runner-erfe68j5.js +21 -0
- package/{dependencyScanner-shs82ynp.js → dependencyScanner-06z2dmrr.js} +2 -2
- package/{devTui-m1atgkc9.js → devTui-q7k7bakd.js} +122 -40
- package/fileSys-wzzrntkw.js +9 -0
- package/guideline.command-kq9gp690.js +42 -0
- package/guidelines/agentRule/agentRule.instruction.md +154 -26
- package/guidelines/conventions/conventions.instruction.md +138 -48
- package/guidelines/mcpRule/mcpRule.instruction.md +7 -5
- package/guidelines/queryRule/queryRule.instruction.md +21 -7
- package/guidelines/runtimeRule/runtimeRule.instruction.md +291 -6
- package/guidelines/ssrRule/ssrRule.instruction.md +4 -2
- package/guidelines/transportRule/transportRule.instruction.md +10 -8
- package/guidelines/workspaceOnboarding/workspaceOnboarding.instruction.md +20 -0
- package/guidelines/workspaceRecipes/workspaceRecipes.instruction.md +66 -3
- package/incrementalBuilder.proc.js +564 -109
- package/{index-2tv3fdx4.js → index-150s07n8.js} +1230 -1459
- package/{index-vyvre9en.js → index-25fvrxmy.js} +6 -2
- package/{index-kcxmv48c.js → index-2ahj896a.js} +45 -79
- package/index-2wc41k5w.js +49 -0
- package/index-3hx1z96c.js +87 -0
- package/{index-q0y2jq9j.js → index-52w0fvh8.js} +21 -4
- package/{index-04bk599g.js → index-5g4rkrvt.js} +10 -17
- package/index-5pvxf7z0.js +53 -0
- package/index-5q14pwzd.js +69 -0
- package/{index-vxtad5bd.js → index-66yspb67.js} +5 -8
- package/index-7md2g9vw.js +6 -0
- package/index-7p4c0yer.js +1058 -0
- package/{index-d43r08tf.js → index-8b639zes.js} +77 -150
- package/{index-ktb5njgy.js → index-8kvy8qb4.js} +49 -99
- package/index-94s080qm.js +266 -0
- package/index-9dp031by.js +12 -0
- package/{index-0g7r8xz3.js → index-a37by9hn.js} +114 -232
- package/{index-k2j1hc4r.js → index-b95yhqjr.js} +12 -12
- package/{index-3d9y066c.js → index-bjwxs904.js} +9 -8
- package/{index-2xp0kfkt.js → index-br8pkrk1.js} +37 -25
- package/index-c14nsvnv.js +700 -0
- package/index-c6eggbee.js +123 -0
- package/index-d2242ksy.js +99 -0
- package/{index-92zztqy1.js → index-dy0jx9ay.js} +2 -17
- package/{index-a64esmtb.js → index-fkvb79qe.js} +523 -1022
- package/index-h54yj9wr.js +1119 -0
- package/{index-by5gpp8x.js → index-hj6w4ka1.js} +2 -1
- package/{index-y6n8nmny.js → index-kn5d4jmg.js} +75 -203
- package/index-m3xsb1ms.js +4300 -0
- package/{index-gyd220p2.js → index-mb59b8x0.js} +554 -577
- package/{index-j9yhg404.js → index-mzjb6zp4.js} +4 -7
- package/index-p6pbz9n5.js +25 -0
- package/{index-sj5vxr8f.js → index-qe71g4ze.js} +983 -898
- package/index-rfcz04w9.js +187 -0
- package/{index-hwkygn9s.js → index-senscydg.js} +331 -36
- package/{index-ffvk10zz.js → index-vs8mefhy.js} +107 -241
- package/index-wa5jbxj1.js +760 -0
- package/index-y5ga5w9t.js +2729 -0
- package/{index-9077wzd2.js → index-y7hx1ygz.js} +43 -26
- package/{index-h7yw2z9n.js → index-zzvvc845.js} +8 -14
- package/index.js +23 -22
- package/{library.command-qzzbj6d8.js → library.command-keqjevpp.js} +4 -5
- package/{localRegistry.command-vyrgg4xw.js → localRegistry.command-pydeq5zf.js} +23 -43
- package/{module.command-rq5hq3ax.js → module.command-dkrnqx3v.js} +8 -12
- package/{package.command-d9hd6v9n.js → package.command-wmc0ys5y.js} +6 -7
- package/package.json +15 -7
- package/{page.command-126d56te.js → page.command-tf3erhrv.js} +4 -5
- package/{primitive.command-taxd1538.js → primitive.command-7hqpegj4.js} +9 -23
- package/prompter-76k56b37.js +8 -0
- package/{quality.command-6gsna0a3.js → quality.command-8122axq3.js} +205 -377
- package/{recipeScanner-fpagtgc1.js → recipeScanner-4b69ybe0.js} +1 -1
- package/{repair.command-tq2dgy72.js → repair.command-mcy12h0n.js} +11 -35
- package/{routeSourceValidator-apyqw7wk.js → routeSourceValidator-7ycnfz0e.js} +0 -6
- package/{scalar.command-3bcmrrkv.js → scalar.command-vvvbzttc.js} +7 -11
- package/skills/akan-code/SKILL.md +130 -0
- 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 +67 -0
- package/skills/akan-ui/SKILL.md +63 -0
- package/skills/akan-validate/SKILL.md +44 -0
- package/{subspace.command-eddzqpx3.js → subspace.command-pzvqfy5h.js} +53 -62
- package/templates/__scalar/__model__/__model__.abstract.md.template +8 -0
- package/templates/app/page/_index.tsx +257 -258
- package/templates/app/page/_layout.tsx +5 -1
- package/templates/app/page/_overrides.tsx +12 -0
- package/templates/app/page/styles.css.template +316 -14
- package/templates/app/public/apple-touch-icon.png +0 -0
- package/templates/app/public/favicon.ico +0 -0
- package/templates/app/public/jelly/cloud.webp +0 -0
- package/templates/app/public/jelly/comet.webp +0 -0
- package/templates/app/public/jelly/moon.webp +0 -0
- package/templates/app/public/jelly/planet.webp +0 -0
- package/templates/app/public/jelly/rocket.webp +0 -0
- package/templates/app/public/jelly/star.webp +0 -0
- package/templates/app/public/jelly/sugar.webp +0 -0
- package/templates/app/public/logo.png +0 -0
- package/templates/app/ui/Recipe/index.ts +9 -0
- package/templates/app/ui/Recipe/jellyButton.ts +63 -0
- package/templates/appSample/lib/__scalar/workHistory/workHistory.abstract.ts +8 -39
- package/templates/appSample/lib/_noti/noti.abstract.ts +6 -29
- package/templates/appSample/lib/task/Task.Unit.tsx +1 -1
- package/templates/appSample/lib/task/Task.Util.tsx +10 -13
- package/templates/appSample/lib/task/Task.View.tsx +2 -2
- package/templates/appSample/lib/task/Task.Zone.tsx +5 -2
- package/templates/appSample/lib/task/task.abstract.ts +12 -32
- package/templates/appSample/lib/task/task.signal.spec.ts +1 -15
- package/templates/appSample/lib/task/task.signal.test.ts +15 -1
- package/templates/appSample/lib/task/task.signal.ts +10 -8
- package/templates/appSample/page/task/[taskId]/_index.tsx +6 -6
- package/templates/appSample/page/task/[taskId]/edit.tsx +5 -4
- package/templates/appSample/page/task/_index.tsx +4 -3
- package/templates/appSample/page/task/_layout.tsx +1 -1
- package/templates/appSample/page/task/new.tsx +5 -4
- package/templates/appSample/srvkit/{AuthGuard.ts → guards.ts} +7 -4
- package/templates/appSample/srvkit/{SessionInternalArg.ts → internalArgs.ts} +6 -4
- package/templates/appSample/ui/GlobalLoading.tsx +2 -1
- package/templates/appSample/ui/QuantityControl.tsx +3 -3
- package/templates/client.ts +7 -30
- package/templates/crudPages/[__model__Id]/edit/page.tsx +2 -1
- package/templates/crudPages/[__model__Id]/page.tsx +2 -1
- package/templates/crudPages/new/page.tsx +2 -1
- package/templates/crudPages/page.tsx +2 -1
- package/templates/crudSinglePage/page.tsx +2 -1
- package/templates/lib/__lib/lib.dictionary.ts +0 -2
- package/templates/lib/__lib/lib.document.ts +0 -2
- package/templates/lib/sig.ts +8 -30
- package/templates/localDev/docker-compose.yaml.template +3 -14
- package/templates/module/__model__.signal.ts +12 -4
- package/templates/pkgRoot/tsconfig.json.template +2 -7
- package/templates/server.ts +1 -3
- package/templates/service/__model__.abstract.md.template +8 -0
- package/templates/workspaceRoot/.gitignore.template +0 -1
- package/templates/workspaceRoot/AGENTS.md.template +6 -0
- package/templates/workspaceRoot/package.json.template +1 -3
- package/tunnel.command-82zz5xnq.js +129 -0
- package/{typeChecker-3xgrzc2v.js → typeChecker-px49mezc.js} +1 -1
- package/typecheck.proc.js +1 -1
- package/workflow.command-y8x3kgjv.js +79 -0
- package/{workspace.command-06wvstgr.js → workspace.command-jvsb7brc.js} +79 -109
- package/abstractCompactor-gmkc0z8j.js +0 -102
- package/application.command-18y0s485.js +0 -204
- package/applicationReleasePackager-4yasxwv9.js +0 -244
- package/capacitorApp-36rbr858.js +0 -12
- package/cloud.command-av97ndh6.js +0 -94
- package/cloud.script-cm11xjdz.js +0 -20
- package/context.command-5v37ef3n.js +0 -81
- package/devTuiView-0hc2emhe.js +0 -9
- package/getDirname-dr3jz1xg.js +0 -7
- package/guideline.command-4pz9g1rt.js +0 -360
- package/guidelines/agentRule/agentRule.generate.json +0 -31
- package/guidelines/componentRule/componentRule.generate.json +0 -31
- package/guidelines/conventions/conventions.generate.json +0 -25
- package/guidelines/cssRule/cssRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.generate.json +0 -31
- package/guidelines/docPageRule/docPageRule.instruction.md +0 -25
- package/guidelines/docSyncRule/docSyncRule.generate.json +0 -31
- package/guidelines/docSyncRule/docSyncRule.instruction.md +0 -25
- package/guidelines/enumConstant/enumConstant.generate.json +0 -31
- package/guidelines/enumConstant/enumConstant.instruction.md +0 -28
- package/guidelines/fieldRule/fieldRule.generate.json +0 -31
- package/guidelines/framework/framework.generate.json +0 -32
- package/guidelines/mcpRule/mcpRule.generate.json +0 -35
- package/guidelines/modelConstant/modelConstant.generate.json +0 -32
- package/guidelines/modelDictionary/modelDictionary.generate.json +0 -32
- package/guidelines/modelDocument/modelDocument.generate.json +0 -31
- package/guidelines/modelService/modelService.generate.json +0 -31
- package/guidelines/modelSignal/modelSignal.generate.json +0 -31
- package/guidelines/modelStore/modelStore.generate.json +0 -31
- package/guidelines/modelTemplate/modelTemplate.generate.json +0 -31
- package/guidelines/modelUnit/modelUnit.generate.json +0 -31
- package/guidelines/modelUtil/modelUtil.generate.json +0 -31
- package/guidelines/modelView/modelView.generate.json +0 -31
- package/guidelines/modelZone/modelZone.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.generate.json +0 -31
- package/guidelines/moduleCodegen/moduleCodegen.instruction.md +0 -28
- package/guidelines/moduleOverview/moduleOverview.generate.json +0 -31
- package/guidelines/queryRule/queryRule.generate.json +0 -31
- package/guidelines/recipeRule/recipeRule.generate.json +0 -31
- package/guidelines/runtimeRule/runtimeRule.generate.json +0 -25
- package/guidelines/scalarConstant/scalarConstant.generate.json +0 -32
- package/guidelines/scalarDictionary/scalarDictionary.generate.json +0 -32
- package/guidelines/scalarModule/scalarModule.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.generate.json +0 -31
- package/guidelines/sharedUiUsage/sharedUiUsage.instruction.md +0 -25
- package/guidelines/ssrRule/ssrRule.generate.json +0 -31
- package/guidelines/transportRule/transportRule.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.generate.json +0 -31
- package/guidelines/utilUiUsage/utilUiUsage.instruction.md +0 -25
- package/guidelines/workspaceOnboarding/workspaceOnboarding.generate.json +0 -24
- package/guidelines/workspaceRecipes/workspaceRecipes.generate.json +0 -33
- package/index-47pj3aee.js +0 -76
- package/index-5ystnsd5.js +0 -841
- package/index-6wwzs1bc.js +0 -7
- package/index-a17x49vg.js +0 -229
- package/index-m0xccmmm.js +0 -228
- package/index-meh558jy.js +0 -65
- package/index-rtxxan88.js +0 -471
- package/index-te54x9zm.js +0 -338
- package/index-w8kmx1b6.js +0 -2176
- package/index-wt7qrtd4.js +0 -1516
- package/templates/__scalar/__model__/__model__.abstract.md +0 -28
- package/templates/module/__model__.abstract.md +0 -28
- package/templates/service/__model__.abstract.md +0 -28
- package/templates/workspaceRoot/patches/react-dom@19.2.7.patch.template +0 -62
- package/workflow.command-gtdnzvbz.js +0 -108
|
@@ -0,0 +1,2729 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
import {
|
|
3
|
+
SubagentPool2,
|
|
4
|
+
akanCodePaths2,
|
|
5
|
+
McpServerConfig,
|
|
6
|
+
McpSignIn
|
|
7
|
+
} from "./index-c14nsvnv.js";
|
|
8
|
+
import {
|
|
9
|
+
stripAnsi
|
|
10
|
+
} from "./index-7md2g9vw.js";
|
|
11
|
+
import {
|
|
12
|
+
akanCodeDefaultModel2,
|
|
13
|
+
akanCodeModel2,
|
|
14
|
+
akanCodeModelSupportsImages2,
|
|
15
|
+
akanCodeModelWarnings2
|
|
16
|
+
} from "./index-c6eggbee.js";
|
|
17
|
+
|
|
18
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgent.ts
|
|
19
|
+
import {
|
|
20
|
+
createAgentSession,
|
|
21
|
+
DefaultResourceLoader
|
|
22
|
+
} from "@earendil-works/pi-coding-agent";
|
|
23
|
+
import {
|
|
24
|
+
codeAgentClip as codeAgentClip5,
|
|
25
|
+
codeAgentLabelChars as codeAgentLabelChars2,
|
|
26
|
+
codeAgentRenderAnswer,
|
|
27
|
+
codeAgentSessionName as codeAgentSessionName2
|
|
28
|
+
} from "akanjs/common";
|
|
29
|
+
|
|
30
|
+
// pkgs/@akanjs/devkit/codeAgent/feedback/DevLogFeedback.ts
|
|
31
|
+
import { existsSync, statSync } from "fs";
|
|
32
|
+
import path from "path";
|
|
33
|
+
|
|
34
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanEditScope.ts
|
|
35
|
+
var empty = { paths: [], apps: [], libs: [], needsSync: false, touchesTsx: false };
|
|
36
|
+
|
|
37
|
+
class AkanEditScope2 {
|
|
38
|
+
static async since(cwd, baseline) {
|
|
39
|
+
const current = await AkanEditScope2.baseline(cwd);
|
|
40
|
+
const changed = [...current].filter((line) => !baseline.has(line));
|
|
41
|
+
if (!changed.length)
|
|
42
|
+
return empty;
|
|
43
|
+
const entries = changed.map((line) => ({ code: line.slice(0, 2), file: line.slice(3).split(" -> ").at(-1) ?? "" }));
|
|
44
|
+
const paths = entries.map((entry) => entry.file).filter(Boolean);
|
|
45
|
+
return {
|
|
46
|
+
paths,
|
|
47
|
+
apps: AkanEditScope2.#targets(paths, "apps"),
|
|
48
|
+
libs: AkanEditScope2.#targets(paths, "libs"),
|
|
49
|
+
needsSync: entries.some((entry) => /[ARD?]/.test(entry.code) && /^(apps|libs)\//.test(entry.file)),
|
|
50
|
+
touchesTsx: paths.some((file) => file.endsWith(".tsx"))
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
static async baseline(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-erfe68j5.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-erfe68j5.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
|
+
const text = typeof result === "string" ? result : JSON.stringify(result, null, 2);
|
|
538
|
+
return { content: [{ type: "text", text }], details: undefined };
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
this.#registerVerify(pi);
|
|
543
|
+
}
|
|
544
|
+
#registerVerify(pi) {
|
|
545
|
+
const verifier = new AkanVerifier2(this.#options.cwd);
|
|
546
|
+
pi.registerTool({
|
|
547
|
+
name: AkanToolPack2.verifyToolName,
|
|
548
|
+
label: "akan verify",
|
|
549
|
+
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.",
|
|
550
|
+
promptSnippet: "akan_verify: run sync/lint/typecheck/quality over the working tree's changes",
|
|
551
|
+
parameters: Type.Object({}),
|
|
552
|
+
execute: async () => {
|
|
553
|
+
const scope = await AkanEditScope2.since(this.#options.cwd, new Set);
|
|
554
|
+
const report = await verifier.verify(scope);
|
|
555
|
+
return {
|
|
556
|
+
content: [{ type: "text", text: AkanVerifier2.summarize(report) }],
|
|
557
|
+
details: undefined,
|
|
558
|
+
isError: !report.ok
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
static async#guidelineNames() {
|
|
564
|
+
try {
|
|
565
|
+
const { Prompter } = await import("./prompter-76k56b37.js");
|
|
566
|
+
return await Prompter.listGuidelines();
|
|
567
|
+
} catch {
|
|
568
|
+
return [];
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AskToolPack.ts
|
|
574
|
+
import { Type as Type2 } from "typebox";
|
|
575
|
+
|
|
576
|
+
class AskToolPack {
|
|
577
|
+
static toolName = "ask_user";
|
|
578
|
+
static maxQuestions = 4;
|
|
579
|
+
#options;
|
|
580
|
+
constructor(options) {
|
|
581
|
+
this.#options = options;
|
|
582
|
+
}
|
|
583
|
+
names() {
|
|
584
|
+
return this.#options.profile.ui.canPrompt ? [AskToolPack.toolName] : [];
|
|
585
|
+
}
|
|
586
|
+
extension() {
|
|
587
|
+
if (!this.#options.profile.ui.canPrompt)
|
|
588
|
+
return;
|
|
589
|
+
return { name: "akan-ask", factory: (pi) => this.#register(pi) };
|
|
590
|
+
}
|
|
591
|
+
#register(pi) {
|
|
592
|
+
pi.registerTool({
|
|
593
|
+
name: AskToolPack.toolName,
|
|
594
|
+
label: "Ask",
|
|
595
|
+
description: "Ask the user to decide something you cannot decide from the code or the request. Use it when two readings of the task would lead to materially different work, not for choices with an obvious default \u2014 and never to ask permission for work you were already asked to do. Offer concrete options whenever you can name them; mark one recommended when you have a view.",
|
|
596
|
+
promptSnippet: "ask_user: put a decision the user owns back to them, with options",
|
|
597
|
+
parameters: Type2.Object({
|
|
598
|
+
questions: Type2.Array(Type2.Object({
|
|
599
|
+
question: Type2.String({ description: "The whole question, as one sentence ending in a question mark." }),
|
|
600
|
+
header: Type2.Optional(Type2.String({ description: "Two or three words naming what is being chosen, e.g. 'Auth method'." })),
|
|
601
|
+
options: Type2.Optional(Type2.Array(Type2.Object({
|
|
602
|
+
label: Type2.String({ description: "The choice, in one to five words." }),
|
|
603
|
+
detail: Type2.Optional(Type2.String({ description: "What picking it means, or the trade-off it carries." })),
|
|
604
|
+
recommended: Type2.Optional(Type2.Boolean({ description: "Mark at most one. It is offered first and labelled as such." }))
|
|
605
|
+
}), { description: "Two to four distinct choices. Leave it out to ask for an answer in prose." })),
|
|
606
|
+
multiSelect: Type2.Optional(Type2.Boolean({ description: "Several options may be chosen together, rather than exactly one." })),
|
|
607
|
+
freeText: Type2.Optional(Type2.Boolean({
|
|
608
|
+
description: "Offer a further choice that lets the user type an answer of their own. Default true."
|
|
609
|
+
}))
|
|
610
|
+
}), { description: `One to ${AskToolPack.maxQuestions} questions, asked in order.` })
|
|
611
|
+
}),
|
|
612
|
+
execute: async (_id, params) => {
|
|
613
|
+
const questions = (params.questions ?? []).slice(0, AskToolPack.maxQuestions);
|
|
614
|
+
if (!questions.length)
|
|
615
|
+
return { content: [{ type: "text", text: "No question was asked." }], details: undefined, isError: true };
|
|
616
|
+
const answers = [];
|
|
617
|
+
for (const entry of questions) {
|
|
618
|
+
const options = (entry.options ?? []).map((option, at) => ({
|
|
619
|
+
key: `${at}`,
|
|
620
|
+
label: option.label,
|
|
621
|
+
...option.detail ? { detail: option.detail } : {},
|
|
622
|
+
...option.recommended ? { recommended: true } : {}
|
|
623
|
+
}));
|
|
624
|
+
const answer = await this.#options.ask({
|
|
625
|
+
prompt: entry.header ? `${entry.header} \u2014 ${entry.question}` : entry.question,
|
|
626
|
+
kind: options.length ? "select" : "text",
|
|
627
|
+
...options.length ? { options } : {},
|
|
628
|
+
...entry.multiSelect ? { multiSelect: true } : {},
|
|
629
|
+
freeText: entry.freeText ?? true
|
|
630
|
+
});
|
|
631
|
+
answers.push(`${entry.question}
|
|
632
|
+
\u2192 ${answer ?? "(skipped \u2014 decide this yourself and say what you chose)"}`);
|
|
633
|
+
}
|
|
634
|
+
return { content: [{ type: "text", text: answers.join(`
|
|
635
|
+
|
|
636
|
+
`) }], details: undefined };
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/MailToolPack.ts
|
|
643
|
+
import { Type as Type3 } from "typebox";
|
|
644
|
+
|
|
645
|
+
class MailToolPack2 {
|
|
646
|
+
static toolNames = ["list_peers", "send_peer_message"];
|
|
647
|
+
#options;
|
|
648
|
+
constructor(options) {
|
|
649
|
+
this.#options = options;
|
|
650
|
+
}
|
|
651
|
+
names() {
|
|
652
|
+
return this.#options.profile.session.store === "file" ? MailToolPack2.toolNames : [];
|
|
653
|
+
}
|
|
654
|
+
extension() {
|
|
655
|
+
if (!this.names().length)
|
|
656
|
+
return;
|
|
657
|
+
return { name: "akan-mail", factory: (pi) => this.#register(pi) };
|
|
658
|
+
}
|
|
659
|
+
#register(pi) {
|
|
660
|
+
pi.registerTool({
|
|
661
|
+
name: "list_peers",
|
|
662
|
+
label: "List peer sessions",
|
|
663
|
+
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.",
|
|
664
|
+
promptSnippet: "list_peers: the other akan code sessions running here",
|
|
665
|
+
parameters: Type3.Object({}),
|
|
666
|
+
execute: async () => ({
|
|
667
|
+
content: [{ type: "text", text: MailToolPack2.#render(this.#options.mailbox()?.peers() ?? []) }],
|
|
668
|
+
details: undefined
|
|
669
|
+
})
|
|
670
|
+
});
|
|
671
|
+
pi.registerTool({
|
|
672
|
+
name: "send_peer_message",
|
|
673
|
+
label: "Message a peer session",
|
|
674
|
+
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.",
|
|
675
|
+
promptSnippet: "send_peer_message: hand work to another akan code session",
|
|
676
|
+
parameters: Type3.Object({
|
|
677
|
+
to: Type3.String({ description: "The peer's name, or the front of its id, from list_peers." }),
|
|
678
|
+
message: Type3.String({ description: "What that session should know or do." })
|
|
679
|
+
}),
|
|
680
|
+
execute: async (_id, params) => {
|
|
681
|
+
const mailbox = this.#options.mailbox();
|
|
682
|
+
const peer = mailbox?.find(params.to);
|
|
683
|
+
if (!mailbox || !peer)
|
|
684
|
+
return {
|
|
685
|
+
content: [{ type: "text", text: `No live session matches "${params.to}". Call list_peers.` }],
|
|
686
|
+
details: undefined,
|
|
687
|
+
isError: true
|
|
688
|
+
};
|
|
689
|
+
try {
|
|
690
|
+
mailbox.send(peer.id, params.message);
|
|
691
|
+
return { content: [{ type: "text", text: `Sent to ${peer.name}.` }], details: undefined };
|
|
692
|
+
} catch (error) {
|
|
693
|
+
return { content: [{ type: "text", text: String(error) }], details: undefined, isError: true };
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
}
|
|
698
|
+
static #render(peers) {
|
|
699
|
+
if (!peers.length)
|
|
700
|
+
return "No other akan code session is running in this workspace.";
|
|
701
|
+
return peers.map((peer) => `${peer.name} (${peer.id.slice(0, 8)}) \u2014 ${peer.cwd}`).join(`
|
|
702
|
+
`);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/McpToolPack.ts
|
|
707
|
+
import {
|
|
708
|
+
codeAgentClip,
|
|
709
|
+
codeAgentOutputChars
|
|
710
|
+
} from "akanjs/common";
|
|
711
|
+
import { Type as Type4 } from "typebox";
|
|
712
|
+
|
|
713
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/McpClient.ts
|
|
714
|
+
var protocolVersion = "2025-06-18";
|
|
715
|
+
|
|
716
|
+
class McpUnauthorized2 extends Error {
|
|
717
|
+
challenge;
|
|
718
|
+
constructor(name, challenge) {
|
|
719
|
+
super(`MCP server "${name}" requires authentication`);
|
|
720
|
+
this.name = "McpUnauthorized";
|
|
721
|
+
this.challenge = challenge;
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
class McpClient2 {
|
|
726
|
+
static maxToolPages = 50;
|
|
727
|
+
ref;
|
|
728
|
+
#proc;
|
|
729
|
+
#nextId = 0;
|
|
730
|
+
#buffer = "";
|
|
731
|
+
#pending = new Map;
|
|
732
|
+
#sessionId;
|
|
733
|
+
#token;
|
|
734
|
+
constructor(ref, token) {
|
|
735
|
+
this.ref = ref;
|
|
736
|
+
this.#token = token;
|
|
737
|
+
}
|
|
738
|
+
async connect() {
|
|
739
|
+
if (this.ref.transport === "stdio")
|
|
740
|
+
this.#spawn();
|
|
741
|
+
const initialize = await this.#call("initialize", {
|
|
742
|
+
protocolVersion,
|
|
743
|
+
capabilities: {},
|
|
744
|
+
clientInfo: { name: "akan-code", version: "1" }
|
|
745
|
+
});
|
|
746
|
+
if (initialize.error)
|
|
747
|
+
throw new Error(initialize.error.message);
|
|
748
|
+
await this.#notify("notifications/initialized");
|
|
749
|
+
return this;
|
|
750
|
+
}
|
|
751
|
+
async listTools() {
|
|
752
|
+
const tools = [];
|
|
753
|
+
let cursor;
|
|
754
|
+
for (let page = 0;page < McpClient2.maxToolPages; page += 1) {
|
|
755
|
+
const response = await this.#call("tools/list", cursor ? { cursor } : {});
|
|
756
|
+
if (response.error)
|
|
757
|
+
throw new Error(response.error.message);
|
|
758
|
+
tools.push(...response.result?.tools ?? []);
|
|
759
|
+
const next = response.result?.nextCursor;
|
|
760
|
+
if (!next || next === cursor)
|
|
761
|
+
return tools;
|
|
762
|
+
cursor = next;
|
|
763
|
+
}
|
|
764
|
+
return tools;
|
|
765
|
+
}
|
|
766
|
+
async callTool(name, args) {
|
|
767
|
+
const response = await this.#call("tools/call", { name, arguments: args });
|
|
768
|
+
if (response.error)
|
|
769
|
+
return { text: response.error.message, isError: true };
|
|
770
|
+
return { text: McpClient2.#renderContent(response.result?.content), isError: !!response.result?.isError };
|
|
771
|
+
}
|
|
772
|
+
close() {
|
|
773
|
+
this.#proc?.kill();
|
|
774
|
+
this.#proc = undefined;
|
|
775
|
+
for (const resolve of this.#pending.values())
|
|
776
|
+
resolve({ error: { code: -1, message: "MCP server closed" } });
|
|
777
|
+
this.#pending.clear();
|
|
778
|
+
}
|
|
779
|
+
#spawn() {
|
|
780
|
+
if (!this.ref.command)
|
|
781
|
+
throw new Error(`MCP server "${this.ref.name}" declares no command`);
|
|
782
|
+
this.#proc = Bun.spawn([this.ref.command, ...this.ref.args ?? []], {
|
|
783
|
+
stdin: "pipe",
|
|
784
|
+
stdout: "pipe",
|
|
785
|
+
stderr: "ignore",
|
|
786
|
+
env: { ...process.env, ...this.ref.env }
|
|
787
|
+
});
|
|
788
|
+
this.#readStdout();
|
|
789
|
+
}
|
|
790
|
+
async#readStdout() {
|
|
791
|
+
const decoder = new TextDecoder;
|
|
792
|
+
for await (const chunk of this.#proc?.stdout ?? []) {
|
|
793
|
+
this.#buffer += decoder.decode(chunk, { stream: true });
|
|
794
|
+
let index = this.#buffer.indexOf(`
|
|
795
|
+
`);
|
|
796
|
+
while (index >= 0) {
|
|
797
|
+
const line = this.#buffer.slice(0, index).trim();
|
|
798
|
+
this.#buffer = this.#buffer.slice(index + 1);
|
|
799
|
+
if (line)
|
|
800
|
+
this.#receive(line);
|
|
801
|
+
index = this.#buffer.indexOf(`
|
|
802
|
+
`);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
#receive(line) {
|
|
807
|
+
try {
|
|
808
|
+
const message = JSON.parse(line);
|
|
809
|
+
if (typeof message.id !== "number")
|
|
810
|
+
return;
|
|
811
|
+
this.#pending.get(message.id)?.(message);
|
|
812
|
+
this.#pending.delete(message.id);
|
|
813
|
+
} catch {}
|
|
814
|
+
}
|
|
815
|
+
async#call(method, params) {
|
|
816
|
+
this.#nextId += 1;
|
|
817
|
+
const id = this.#nextId;
|
|
818
|
+
const request = { jsonrpc: "2.0", id, method, params };
|
|
819
|
+
if (this.ref.transport === "http")
|
|
820
|
+
return await this.#post(request);
|
|
821
|
+
this.#proc?.stdin.write(`${JSON.stringify(request)}
|
|
822
|
+
`);
|
|
823
|
+
return await new Promise((resolve) => {
|
|
824
|
+
this.#pending.set(id, resolve);
|
|
825
|
+
setTimeout(() => {
|
|
826
|
+
if (!this.#pending.delete(id))
|
|
827
|
+
return;
|
|
828
|
+
resolve({ error: { code: -2, message: `MCP server "${this.ref.name}" timed out on ${method}` } });
|
|
829
|
+
}, 30000);
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
async#notify(method) {
|
|
833
|
+
const request = { jsonrpc: "2.0", method };
|
|
834
|
+
if (this.ref.transport === "http")
|
|
835
|
+
await this.#post(request);
|
|
836
|
+
else
|
|
837
|
+
this.#proc?.stdin.write(`${JSON.stringify(request)}
|
|
838
|
+
`);
|
|
839
|
+
}
|
|
840
|
+
async#post(body) {
|
|
841
|
+
if (!this.ref.url)
|
|
842
|
+
throw new Error(`MCP server "${this.ref.name}" declares no url`);
|
|
843
|
+
const response = await fetch(this.ref.url, {
|
|
844
|
+
method: "POST",
|
|
845
|
+
headers: {
|
|
846
|
+
"content-type": "application/json",
|
|
847
|
+
accept: "application/json, text/event-stream",
|
|
848
|
+
"mcp-protocol-version": protocolVersion,
|
|
849
|
+
...this.ref.headers,
|
|
850
|
+
...this.#token ? { authorization: `Bearer ${this.#token}` } : {},
|
|
851
|
+
...this.#sessionId ? { "mcp-session-id": this.#sessionId } : {}
|
|
852
|
+
},
|
|
853
|
+
body: JSON.stringify(body),
|
|
854
|
+
signal: AbortSignal.timeout(30000)
|
|
855
|
+
});
|
|
856
|
+
if (response.status === 401)
|
|
857
|
+
throw new McpUnauthorized2(this.ref.name, response.headers.get("www-authenticate") ?? "");
|
|
858
|
+
this.#sessionId = response.headers.get("mcp-session-id") ?? this.#sessionId;
|
|
859
|
+
const text = await response.text();
|
|
860
|
+
if (!text.trim())
|
|
861
|
+
return {};
|
|
862
|
+
const payload = text.startsWith("event:") || text.startsWith("data:") ? McpClient2.#firstSseData(text) : text;
|
|
863
|
+
return payload ? JSON.parse(payload) : {};
|
|
864
|
+
}
|
|
865
|
+
static #firstSseData(text) {
|
|
866
|
+
return text.split(`
|
|
867
|
+
`).find((line) => line.startsWith("data:"))?.slice(5).trim();
|
|
868
|
+
}
|
|
869
|
+
static #renderContent(content) {
|
|
870
|
+
if (!Array.isArray(content))
|
|
871
|
+
return typeof content === "string" ? content : JSON.stringify(content ?? null);
|
|
872
|
+
return content.map((part) => part.type === "text" ? part.text ?? "" : JSON.stringify(part)).join(`
|
|
873
|
+
`);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/McpToolPack.ts
|
|
878
|
+
class McpToolPack2 {
|
|
879
|
+
#clients = [];
|
|
880
|
+
#status = [];
|
|
881
|
+
#onNotice;
|
|
882
|
+
constructor(onNotice) {
|
|
883
|
+
this.#onNotice = onNotice;
|
|
884
|
+
}
|
|
885
|
+
static async connect(options) {
|
|
886
|
+
const refs = McpToolPack2.#refs(options);
|
|
887
|
+
if (!refs.length)
|
|
888
|
+
return;
|
|
889
|
+
const pack = new McpToolPack2(options.onNotice);
|
|
890
|
+
for (const ref of refs) {
|
|
891
|
+
const base = { name: ref.name, transport: ref.transport, target: McpServerConfig.targetOf(ref) };
|
|
892
|
+
const token = ref.transport === "http" ? await McpSignIn.token(ref, options.onNotice) : undefined;
|
|
893
|
+
try {
|
|
894
|
+
const opened = await McpToolPack2.#openRefreshing(ref, token, options.onNotice);
|
|
895
|
+
const status = {
|
|
896
|
+
...base,
|
|
897
|
+
tools: opened.tools.map((tool) => McpToolPack2.#nameOf(ref.name, tool.name)),
|
|
898
|
+
auth: opened.token ? "authorized" : "none"
|
|
899
|
+
};
|
|
900
|
+
pack.#clients.push({ ...opened, status, renewing: undefined, refused: undefined });
|
|
901
|
+
pack.#status.push(status);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
if (error instanceof McpUnauthorized2) {
|
|
904
|
+
options.onNotice?.(`MCP server "${ref.name}" needs signing in \u2014 /mcp login ${ref.name}`);
|
|
905
|
+
pack.#status.push({ ...base, tools: [], auth: "required" });
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
options.onNotice?.(`MCP server "${ref.name}" is unavailable: ${String(error)}`);
|
|
909
|
+
pack.#status.push({ ...base, tools: [], error: String(error) });
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
return pack;
|
|
913
|
+
}
|
|
914
|
+
static async#openRefreshing(ref, token, onNotice) {
|
|
915
|
+
try {
|
|
916
|
+
return await McpToolPack2.#open(ref, token);
|
|
917
|
+
} catch (error) {
|
|
918
|
+
if (!(error instanceof McpUnauthorized2) || !token)
|
|
919
|
+
throw error;
|
|
920
|
+
const retry = await McpSignIn.retryToken(ref, token, onNotice);
|
|
921
|
+
if (!retry)
|
|
922
|
+
throw error;
|
|
923
|
+
return await McpToolPack2.#open(ref, retry);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
static async#open(ref, token) {
|
|
927
|
+
const client = await new McpClient2(ref, token).connect();
|
|
928
|
+
return { client, tools: await client.listTools(), token };
|
|
929
|
+
}
|
|
930
|
+
get toolNames() {
|
|
931
|
+
return this.#clients.flatMap(({ client, tools }) => tools.map((tool) => McpToolPack2.#nameOf(client.ref.name, tool.name)));
|
|
932
|
+
}
|
|
933
|
+
get status() {
|
|
934
|
+
return this.#status;
|
|
935
|
+
}
|
|
936
|
+
extension() {
|
|
937
|
+
return { name: "akan-mcp", factory: (pi) => this.#register(pi) };
|
|
938
|
+
}
|
|
939
|
+
close() {
|
|
940
|
+
for (const { client } of this.#clients)
|
|
941
|
+
client.close();
|
|
942
|
+
}
|
|
943
|
+
#register(pi) {
|
|
944
|
+
for (const server of this.#clients)
|
|
945
|
+
for (const tool of server.tools) {
|
|
946
|
+
const name = McpToolPack2.#nameOf(server.client.ref.name, tool.name);
|
|
947
|
+
const description = tool.description ?? tool.name;
|
|
948
|
+
pi.registerTool({
|
|
949
|
+
name,
|
|
950
|
+
label: `${server.client.ref.name}: ${tool.name}`,
|
|
951
|
+
description,
|
|
952
|
+
promptSnippet: `${name}: ${description.split(".")[0] ?? description}`,
|
|
953
|
+
parameters: Type4.Unsafe(tool.inputSchema),
|
|
954
|
+
execute: async (_id, params) => {
|
|
955
|
+
const result = await this.#call(server, tool.name, params ?? {});
|
|
956
|
+
return {
|
|
957
|
+
content: [{ type: "text", text: codeAgentClip(result.text, codeAgentOutputChars) }],
|
|
958
|
+
details: undefined,
|
|
959
|
+
isError: result.isError
|
|
960
|
+
};
|
|
961
|
+
}
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
async#call(server, tool, args) {
|
|
966
|
+
const client = server.client;
|
|
967
|
+
try {
|
|
968
|
+
return await client.callTool(tool, args);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
if (!(error instanceof McpUnauthorized2))
|
|
971
|
+
throw error;
|
|
972
|
+
}
|
|
973
|
+
const renewed = await this.#renewed(server, client);
|
|
974
|
+
try {
|
|
975
|
+
if (renewed)
|
|
976
|
+
return await renewed.callTool(tool, args);
|
|
977
|
+
} catch (error) {
|
|
978
|
+
if (!(error instanceof McpUnauthorized2))
|
|
979
|
+
throw error;
|
|
980
|
+
McpToolPack2.#refuse(server, renewed);
|
|
981
|
+
}
|
|
982
|
+
throw new Error(`MCP server "${client.ref.name}" needs signing in \u2014 /mcp login ${client.ref.name}`);
|
|
983
|
+
}
|
|
984
|
+
static #refuse(server, client) {
|
|
985
|
+
server.refused = client;
|
|
986
|
+
server.status.auth = "required";
|
|
987
|
+
}
|
|
988
|
+
async#renewed(server, refused) {
|
|
989
|
+
if (server.client === refused && server.refused !== refused)
|
|
990
|
+
server.renewing ??= this.#renew(server, refused).finally(() => {
|
|
991
|
+
server.renewing = undefined;
|
|
992
|
+
});
|
|
993
|
+
if (server.renewing)
|
|
994
|
+
return await server.renewing;
|
|
995
|
+
return server.client === server.refused ? undefined : server.client;
|
|
996
|
+
}
|
|
997
|
+
async#renew(server, refused) {
|
|
998
|
+
const token = server.token ? await McpSignIn.retryToken(refused.ref, server.token, this.#onNotice) : undefined;
|
|
999
|
+
const client = token ? await McpToolPack2.#reopen(refused.ref, token) : undefined;
|
|
1000
|
+
if (!client) {
|
|
1001
|
+
McpToolPack2.#refuse(server, refused);
|
|
1002
|
+
return;
|
|
1003
|
+
}
|
|
1004
|
+
refused.close();
|
|
1005
|
+
server.client = client;
|
|
1006
|
+
server.token = token;
|
|
1007
|
+
return client;
|
|
1008
|
+
}
|
|
1009
|
+
static async#reopen(ref, token) {
|
|
1010
|
+
try {
|
|
1011
|
+
return await new McpClient2(ref, token).connect();
|
|
1012
|
+
} catch (error) {
|
|
1013
|
+
if (error instanceof McpUnauthorized2)
|
|
1014
|
+
return;
|
|
1015
|
+
throw error;
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
static #nameOf(server, tool) {
|
|
1019
|
+
return `mcp__${server.replace(/[^a-zA-Z0-9_]/g, "_")}__${tool}`;
|
|
1020
|
+
}
|
|
1021
|
+
static #refs(options) {
|
|
1022
|
+
if (options.profile.tools.mcp === "off")
|
|
1023
|
+
return [];
|
|
1024
|
+
const declared = Array.isArray(options.profile.tools.mcp) ? options.profile.tools.mcp : [];
|
|
1025
|
+
return [...McpServerConfig.read(options.workspaceRoot), ...declared];
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/SessionToolPack.ts
|
|
1030
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
1031
|
+
import { codeAgentClip as codeAgentClip2, codeAgentOutputChars as codeAgentOutputChars2 } from "akanjs/common";
|
|
1032
|
+
import { Type as Type5 } from "typebox";
|
|
1033
|
+
var snippetChars = 240;
|
|
1034
|
+
|
|
1035
|
+
class SessionToolPack2 {
|
|
1036
|
+
#options;
|
|
1037
|
+
constructor(options) {
|
|
1038
|
+
this.#options = options;
|
|
1039
|
+
}
|
|
1040
|
+
static toolNames = ["session_search", "session_read"];
|
|
1041
|
+
names() {
|
|
1042
|
+
return this.#options.profile.session.crossSession ? SessionToolPack2.toolNames : [];
|
|
1043
|
+
}
|
|
1044
|
+
extension() {
|
|
1045
|
+
if (!this.names().length)
|
|
1046
|
+
return;
|
|
1047
|
+
return { name: "akan-sessions", factory: (pi) => this.#register(pi) };
|
|
1048
|
+
}
|
|
1049
|
+
#register(pi) {
|
|
1050
|
+
pi.registerTool({
|
|
1051
|
+
name: "session_search",
|
|
1052
|
+
label: "Search sessions",
|
|
1053
|
+
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.",
|
|
1054
|
+
promptSnippet: "session_search: find earlier conversations in this workspace by phrase",
|
|
1055
|
+
parameters: Type5.Object({
|
|
1056
|
+
query: Type5.String({ description: "A phrase to look for, matched case-insensitively." }),
|
|
1057
|
+
limit: Type5.Optional(Type5.Number({ description: "How many sessions to return, default 5." }))
|
|
1058
|
+
}),
|
|
1059
|
+
execute: async (_id, params) => {
|
|
1060
|
+
const matches = await this.#search(params.query, params.limit ?? 5);
|
|
1061
|
+
return { content: [{ type: "text", text: matches }], details: undefined };
|
|
1062
|
+
}
|
|
1063
|
+
});
|
|
1064
|
+
pi.registerTool({
|
|
1065
|
+
name: "session_read",
|
|
1066
|
+
label: "Read session",
|
|
1067
|
+
description: "Read one earlier conversation, found with session_search, as plain text.",
|
|
1068
|
+
promptSnippet: "session_read: read one earlier conversation by id",
|
|
1069
|
+
parameters: Type5.Object({ sessionId: Type5.String() }),
|
|
1070
|
+
execute: async (_id, params) => {
|
|
1071
|
+
const text = await this.#read(params.sessionId);
|
|
1072
|
+
return { content: [{ type: "text", text }], details: undefined, isError: text.startsWith("No session") };
|
|
1073
|
+
}
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1076
|
+
async#search(query, limit) {
|
|
1077
|
+
const needle = query.toLowerCase();
|
|
1078
|
+
const sessions = (await this.#list()).filter((session) => session.id !== this.#options.currentSessionId() && session.allMessagesText.toLowerCase().includes(needle));
|
|
1079
|
+
if (!sessions.length)
|
|
1080
|
+
return `No earlier session mentions "${query}".`;
|
|
1081
|
+
return sessions.slice(0, limit).map((session) => {
|
|
1082
|
+
const at = session.allMessagesText.toLowerCase().indexOf(needle);
|
|
1083
|
+
const snippet = session.allMessagesText.slice(Math.max(0, at - 80), at + snippetChars).replace(/\s+/g, " ");
|
|
1084
|
+
return `- ${session.id} (${session.modified.toISOString().slice(0, 10)}${session.name ? `, "${session.name}"` : ""})
|
|
1085
|
+
\u2026${snippet}\u2026`;
|
|
1086
|
+
}).join(`
|
|
1087
|
+
`);
|
|
1088
|
+
}
|
|
1089
|
+
async#read(sessionId) {
|
|
1090
|
+
const session = (await this.#list()).find((entry) => entry.id === sessionId);
|
|
1091
|
+
if (!session)
|
|
1092
|
+
return `No session with id ${sessionId} in this workspace.`;
|
|
1093
|
+
return codeAgentClip2(session.allMessagesText, codeAgentOutputChars2 * 2);
|
|
1094
|
+
}
|
|
1095
|
+
async#list() {
|
|
1096
|
+
return await SessionManager.list(this.#options.cwd, akanCodePaths2.sessionsDir(this.#options.workspaceRoot));
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/WebToolPack.ts
|
|
1101
|
+
import { codeAgentClip as codeAgentClip3, codeAgentOutputChars as codeAgentOutputChars3 } from "akanjs/common";
|
|
1102
|
+
import { Type as Type6 } from "typebox";
|
|
1103
|
+
|
|
1104
|
+
class WebToolPack2 {
|
|
1105
|
+
#options;
|
|
1106
|
+
constructor(options) {
|
|
1107
|
+
this.#options = options;
|
|
1108
|
+
}
|
|
1109
|
+
static providers = [
|
|
1110
|
+
{
|
|
1111
|
+
name: "brave",
|
|
1112
|
+
envKey: "BRAVE_API_KEY",
|
|
1113
|
+
search: async (key, query, count) => {
|
|
1114
|
+
const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${count}`;
|
|
1115
|
+
const response = await fetch(url, { headers: { "x-subscription-token": key, accept: "application/json" } });
|
|
1116
|
+
const body = await response.json();
|
|
1117
|
+
return (body.web?.results ?? []).map((r) => `- ${r.title}
|
|
1118
|
+
${r.url}
|
|
1119
|
+
${r.description}`).join(`
|
|
1120
|
+
`);
|
|
1121
|
+
}
|
|
1122
|
+
},
|
|
1123
|
+
{
|
|
1124
|
+
name: "tavily",
|
|
1125
|
+
envKey: "TAVILY_API_KEY",
|
|
1126
|
+
search: async (key, query, count) => {
|
|
1127
|
+
const response = await fetch("https://api.tavily.com/search", {
|
|
1128
|
+
method: "POST",
|
|
1129
|
+
headers: { "content-type": "application/json" },
|
|
1130
|
+
body: JSON.stringify({ api_key: key, query, max_results: count })
|
|
1131
|
+
});
|
|
1132
|
+
const body = await response.json();
|
|
1133
|
+
return (body.results ?? []).map((r) => `- ${r.title}
|
|
1134
|
+
${r.url}
|
|
1135
|
+
${r.content}`).join(`
|
|
1136
|
+
`);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
];
|
|
1140
|
+
names() {
|
|
1141
|
+
const names = [];
|
|
1142
|
+
if (this.#options.profile.tools.web.fetch)
|
|
1143
|
+
names.push("web_fetch");
|
|
1144
|
+
if (this.#options.profile.tools.web.search && WebToolPack2.#provider())
|
|
1145
|
+
names.push("web_search");
|
|
1146
|
+
return names;
|
|
1147
|
+
}
|
|
1148
|
+
extension() {
|
|
1149
|
+
if (!this.names().length)
|
|
1150
|
+
return;
|
|
1151
|
+
return { name: "akan-web", factory: (pi) => this.#register(pi) };
|
|
1152
|
+
}
|
|
1153
|
+
#register(pi) {
|
|
1154
|
+
if (this.#options.profile.tools.web.fetch)
|
|
1155
|
+
this.#registerFetch(pi);
|
|
1156
|
+
const provider = WebToolPack2.#provider();
|
|
1157
|
+
if (this.#options.profile.tools.web.search && provider)
|
|
1158
|
+
this.#registerSearch(pi, provider);
|
|
1159
|
+
}
|
|
1160
|
+
#registerFetch(pi) {
|
|
1161
|
+
const allowHosts = this.#options.profile.network.allowHosts;
|
|
1162
|
+
pi.registerTool({
|
|
1163
|
+
name: "web_fetch",
|
|
1164
|
+
label: "Fetch",
|
|
1165
|
+
description: "Fetch a URL and return its readable text. HTML is stripped to text; the result is truncated.",
|
|
1166
|
+
promptSnippet: "web_fetch: fetch a URL and return its readable text",
|
|
1167
|
+
parameters: Type6.Object({ url: Type6.String({ description: "Absolute http(s) URL." }) }),
|
|
1168
|
+
execute: async (_id, params) => {
|
|
1169
|
+
const refusal = WebToolPack2.#refuseHost(params.url, allowHosts);
|
|
1170
|
+
if (refusal)
|
|
1171
|
+
return { content: [{ type: "text", text: refusal }], details: undefined, isError: true };
|
|
1172
|
+
const response = await fetch(params.url, { signal: AbortSignal.timeout(20000), redirect: "follow" });
|
|
1173
|
+
const text = WebToolPack2.#readable(await response.text());
|
|
1174
|
+
return {
|
|
1175
|
+
content: [
|
|
1176
|
+
{ type: "text", text: `${response.status} ${params.url}
|
|
1177
|
+
|
|
1178
|
+
${codeAgentClip3(text, codeAgentOutputChars3)}` }
|
|
1179
|
+
],
|
|
1180
|
+
details: undefined,
|
|
1181
|
+
isError: !response.ok
|
|
1182
|
+
};
|
|
1183
|
+
}
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
#registerSearch(pi, provider) {
|
|
1187
|
+
pi.registerTool({
|
|
1188
|
+
name: "web_search",
|
|
1189
|
+
label: "Search",
|
|
1190
|
+
description: `Search the web (${provider.name}) and return the top results as title, url and snippet.`,
|
|
1191
|
+
promptSnippet: "web_search: search the web and return titles, urls and snippets",
|
|
1192
|
+
parameters: Type6.Object({
|
|
1193
|
+
query: Type6.String(),
|
|
1194
|
+
count: Type6.Optional(Type6.Number({ description: "How many results, default 5." }))
|
|
1195
|
+
}),
|
|
1196
|
+
execute: async (_id, params) => {
|
|
1197
|
+
const key = process.env[provider.envKey] ?? "";
|
|
1198
|
+
const results = await provider.search(key, params.query, params.count ?? 5);
|
|
1199
|
+
return {
|
|
1200
|
+
content: [{ type: "text", text: codeAgentClip3(results || "(no results)", codeAgentOutputChars3) }],
|
|
1201
|
+
details: undefined
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
static #provider() {
|
|
1207
|
+
return WebToolPack2.providers.find((provider) => !!process.env[provider.envKey]);
|
|
1208
|
+
}
|
|
1209
|
+
static #refuseHost(url, allowHosts) {
|
|
1210
|
+
let parsed;
|
|
1211
|
+
try {
|
|
1212
|
+
parsed = new URL(url);
|
|
1213
|
+
} catch {
|
|
1214
|
+
return `"${url}" is not an absolute URL.`;
|
|
1215
|
+
}
|
|
1216
|
+
if (parsed.protocol !== "http:" && parsed.protocol !== "https:")
|
|
1217
|
+
return "Only http and https URLs can be fetched.";
|
|
1218
|
+
if (allowHosts?.length && !allowHosts.includes(parsed.hostname))
|
|
1219
|
+
return `This profile only allows ${allowHosts.join(", ")}.`;
|
|
1220
|
+
return;
|
|
1221
|
+
}
|
|
1222
|
+
static #readable(html) {
|
|
1223
|
+
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, `
|
|
1224
|
+
|
|
1225
|
+
`).trim();
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanCodePlugins.ts
|
|
1230
|
+
class AkanCodePlugins2 {
|
|
1231
|
+
static async build(options) {
|
|
1232
|
+
const mcp = await McpToolPack2.connect({
|
|
1233
|
+
workspaceRoot: options.workspace.workspaceRoot,
|
|
1234
|
+
profile: options.profile,
|
|
1235
|
+
...options.onNotice ? { onNotice: options.onNotice } : {}
|
|
1236
|
+
});
|
|
1237
|
+
const dispose = () => mcp?.close();
|
|
1238
|
+
const mcpStatus = mcp?.status ?? [];
|
|
1239
|
+
const web = new WebToolPack2({ profile: options.profile });
|
|
1240
|
+
const sessions = new SessionToolPack2({
|
|
1241
|
+
workspaceRoot: options.workspace.workspaceRoot,
|
|
1242
|
+
cwd: options.cwd,
|
|
1243
|
+
profile: options.profile,
|
|
1244
|
+
currentSessionId: options.currentSessionId
|
|
1245
|
+
});
|
|
1246
|
+
const mail = new MailToolPack2({ profile: options.profile, mailbox: options.mailbox });
|
|
1247
|
+
const onAsk = options.onAsk;
|
|
1248
|
+
const ask = onAsk ? new AskToolPack({ profile: options.profile, ask: onAsk }) : undefined;
|
|
1249
|
+
const shared = {
|
|
1250
|
+
extensions: [mcp?.extension(), web.extension(), sessions.extension(), mail.extension(), ask?.extension()].filter((entry) => !!entry),
|
|
1251
|
+
toolNames: [
|
|
1252
|
+
...mcp?.toolNames ?? [],
|
|
1253
|
+
...web.names(),
|
|
1254
|
+
...sessions.names(),
|
|
1255
|
+
...mail.names(),
|
|
1256
|
+
...ask?.names() ?? []
|
|
1257
|
+
]
|
|
1258
|
+
};
|
|
1259
|
+
if (!options.profile.tools.akan)
|
|
1260
|
+
return { ...shared, mcp: mcpStatus, dispose };
|
|
1261
|
+
const readOnly = !options.profile.tools.builtin.includes("write");
|
|
1262
|
+
const pack = new AkanToolPack2({
|
|
1263
|
+
workspace: options.workspace,
|
|
1264
|
+
cwd: options.cwd,
|
|
1265
|
+
mode: readOnly ? "readonly" : "apply"
|
|
1266
|
+
});
|
|
1267
|
+
const extensions = [pack.extension()];
|
|
1268
|
+
const subagent = SubagentPool2.extensionFor({
|
|
1269
|
+
workspace: options.workspace,
|
|
1270
|
+
cwd: options.cwd,
|
|
1271
|
+
parent: options.profile,
|
|
1272
|
+
depth: options.depth,
|
|
1273
|
+
...options.subagentSpend ? { spend: options.subagentSpend } : {},
|
|
1274
|
+
...options.onNotice ? { onNotice: options.onNotice } : {},
|
|
1275
|
+
...options.onAgents ? { onAgents: options.onAgents } : {}
|
|
1276
|
+
});
|
|
1277
|
+
if (subagent)
|
|
1278
|
+
extensions.push(subagent);
|
|
1279
|
+
const sources = await AkanCodePlugins2.#sources(options, readOnly);
|
|
1280
|
+
if (sources.length)
|
|
1281
|
+
extensions.push(new TurnFeedback2(sources, {
|
|
1282
|
+
budget: options.profile.limits.feedback,
|
|
1283
|
+
...options.onNotice ? { onNotice: options.onNotice } : {}
|
|
1284
|
+
}).extension());
|
|
1285
|
+
return {
|
|
1286
|
+
extensions: [...extensions, ...shared.extensions],
|
|
1287
|
+
toolNames: [...await pack.names(), ...subagent ? [SubagentPool2.toolName] : [], ...shared.toolNames],
|
|
1288
|
+
mcp: mcpStatus,
|
|
1289
|
+
dispose
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
static async#sources(options, readOnly) {
|
|
1293
|
+
const sources = [
|
|
1294
|
+
new VerifyFeedback2({ cwd: options.cwd, readOnly }),
|
|
1295
|
+
new DevLogFeedback2({ workspaceRoot: options.workspace.workspaceRoot, cwd: options.cwd, apps: options.apps })
|
|
1296
|
+
];
|
|
1297
|
+
const previewUrl = await AkanCodePlugins2.#previewUrl(options);
|
|
1298
|
+
if (!previewUrl)
|
|
1299
|
+
return sources;
|
|
1300
|
+
const reason = PreviewView2.unavailableReason();
|
|
1301
|
+
if (reason)
|
|
1302
|
+
options.onNotice?.(reason);
|
|
1303
|
+
else
|
|
1304
|
+
sources.push(new PreviewFeedback2({ cwd: options.cwd, previewUrl, canSeeImages: options.canSeeImages }));
|
|
1305
|
+
return sources;
|
|
1306
|
+
}
|
|
1307
|
+
static async#previewUrl(options) {
|
|
1308
|
+
for (const app of options.apps) {
|
|
1309
|
+
const url = await DevLogFeedback2.previewUrl(options.workspace.workspaceRoot, app);
|
|
1310
|
+
if (url)
|
|
1311
|
+
return url;
|
|
1312
|
+
}
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/AkanCodeServices.ts
|
|
1318
|
+
import { existsSync as existsSync7, mkdirSync, mkdtempSync } from "fs";
|
|
1319
|
+
import { tmpdir } from "os";
|
|
1320
|
+
import path7 from "path";
|
|
1321
|
+
import { ModelRuntime, SessionManager as SessionManager2, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
1322
|
+
|
|
1323
|
+
// pkgs/@akanjs/devkit/codeAgent/tools/AkanEnvKeys.ts
|
|
1324
|
+
import { existsSync as existsSync4, readFileSync } from "fs";
|
|
1325
|
+
import path4 from "path";
|
|
1326
|
+
|
|
1327
|
+
class AkanEnvKeys2 {
|
|
1328
|
+
static async apply(runtime, workspaceRoot) {
|
|
1329
|
+
const applied = [];
|
|
1330
|
+
for (const [name, value] of AkanEnvKeys2.#entries(workspaceRoot)) {
|
|
1331
|
+
const provider = AkanEnvKeys2.#providerOf(name);
|
|
1332
|
+
if (!provider || !value)
|
|
1333
|
+
continue;
|
|
1334
|
+
await runtime.setRuntimeApiKey(provider, value);
|
|
1335
|
+
applied.push(provider);
|
|
1336
|
+
}
|
|
1337
|
+
return applied;
|
|
1338
|
+
}
|
|
1339
|
+
static #entries(workspaceRoot) {
|
|
1340
|
+
const fromProcess = Object.entries(process.env).filter(([, value]) => typeof value === "string");
|
|
1341
|
+
const file = path4.join(workspaceRoot, ".env");
|
|
1342
|
+
if (!existsSync4(file))
|
|
1343
|
+
return fromProcess;
|
|
1344
|
+
const fromFile = readFileSync(file, "utf8").split(`
|
|
1345
|
+
`).map((line) => line.trim()).filter((line) => line && !line.startsWith("#") && line.includes("=")).map((line) => {
|
|
1346
|
+
const index = line.indexOf("=");
|
|
1347
|
+
return [
|
|
1348
|
+
line.slice(0, index).trim(),
|
|
1349
|
+
line.slice(index + 1).trim().replace(/^["']|["']$/g, "")
|
|
1350
|
+
];
|
|
1351
|
+
});
|
|
1352
|
+
return [...fromFile, ...fromProcess];
|
|
1353
|
+
}
|
|
1354
|
+
static #providerOf(name) {
|
|
1355
|
+
const match = /^([A-Z0-9_]+)_API_KEY$/.exec(name);
|
|
1356
|
+
return match?.[1]?.toLowerCase().replace(/_/g, "-");
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/AkanContextFiles.ts
|
|
1361
|
+
import { existsSync as existsSync5, readdirSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
1362
|
+
import path5 from "path";
|
|
1363
|
+
|
|
1364
|
+
class AkanContextFiles {
|
|
1365
|
+
static #frontmatter = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/;
|
|
1366
|
+
static extend(base, options) {
|
|
1367
|
+
const seen = new Set(base.map((file) => file.path));
|
|
1368
|
+
const contents = new Set(base.map((file) => file.content.trim()));
|
|
1369
|
+
const added = [];
|
|
1370
|
+
for (const file of [...AkanContextFiles.#claude(base), ...AkanContextFiles.#cursor(options)]) {
|
|
1371
|
+
if (seen.has(file.path) || contents.has(file.content.trim()) || !file.content.trim())
|
|
1372
|
+
continue;
|
|
1373
|
+
seen.add(file.path);
|
|
1374
|
+
contents.add(file.content.trim());
|
|
1375
|
+
added.push(file);
|
|
1376
|
+
}
|
|
1377
|
+
return [...base, ...added];
|
|
1378
|
+
}
|
|
1379
|
+
static #claude(base) {
|
|
1380
|
+
const files = [];
|
|
1381
|
+
for (const file of base) {
|
|
1382
|
+
if (!/^AGENTS\.(md|MD)$/.test(path5.basename(file.path)))
|
|
1383
|
+
continue;
|
|
1384
|
+
for (const name of ["CLAUDE.md", "CLAUDE.MD"]) {
|
|
1385
|
+
const candidate = path5.join(path5.dirname(file.path), name);
|
|
1386
|
+
const content = AkanContextFiles.#read(candidate);
|
|
1387
|
+
if (content !== undefined)
|
|
1388
|
+
files.push({ path: candidate, content });
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
return files;
|
|
1392
|
+
}
|
|
1393
|
+
static #cursor(options) {
|
|
1394
|
+
const files = [];
|
|
1395
|
+
const roots = [...new Set([options.workspaceRoot, options.cwd])];
|
|
1396
|
+
for (const root of roots) {
|
|
1397
|
+
const legacy = AkanContextFiles.#read(path5.join(root, ".cursorrules"));
|
|
1398
|
+
if (legacy !== undefined)
|
|
1399
|
+
files.push({ path: path5.join(root, ".cursorrules"), content: legacy });
|
|
1400
|
+
for (const file of AkanContextFiles.#mdcFiles(path5.join(root, ".cursor", "rules"))) {
|
|
1401
|
+
const raw = AkanContextFiles.#read(file);
|
|
1402
|
+
if (raw === undefined)
|
|
1403
|
+
continue;
|
|
1404
|
+
const body = AkanContextFiles.#applied(raw);
|
|
1405
|
+
if (body !== undefined)
|
|
1406
|
+
files.push({ path: file, content: body });
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
return files;
|
|
1410
|
+
}
|
|
1411
|
+
static #applied(raw) {
|
|
1412
|
+
const match = AkanContextFiles.#frontmatter.exec(raw);
|
|
1413
|
+
if (!match)
|
|
1414
|
+
return raw;
|
|
1415
|
+
if (!/^\s*alwaysApply\s*:\s*true\s*$/m.test(match[1] ?? ""))
|
|
1416
|
+
return;
|
|
1417
|
+
return raw.slice(match[0].length);
|
|
1418
|
+
}
|
|
1419
|
+
static #mdcFiles(dir) {
|
|
1420
|
+
if (!existsSync5(dir))
|
|
1421
|
+
return [];
|
|
1422
|
+
try {
|
|
1423
|
+
return readdirSync(dir, { recursive: true, withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.endsWith(".mdc")).map((entry) => path5.join(entry.parentPath ?? dir, entry.name)).sort();
|
|
1424
|
+
} catch {
|
|
1425
|
+
return [];
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
static #read(file) {
|
|
1429
|
+
try {
|
|
1430
|
+
if (!existsSync5(file) || !statSync2(file).isFile())
|
|
1431
|
+
return;
|
|
1432
|
+
return readFileSync2(file, "utf8").replace(/^\uFEFF/, "");
|
|
1433
|
+
} catch {
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/akanSystemPrompt.ts
|
|
1440
|
+
var akanSystemPrompt2 = (profile) => {
|
|
1441
|
+
const lines = [
|
|
1442
|
+
"You are the akan coding agent, running inside an Akan.js workspace.",
|
|
1443
|
+
"",
|
|
1444
|
+
"- 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.",
|
|
1445
|
+
"- Read context with `inspect_akan_context` before reading source bodies; it answers most structural questions without spending the file.",
|
|
1446
|
+
"- Fetch the guideline for an area before a deep pass on it: `get_guideline`.",
|
|
1447
|
+
"- A question about `akan code` itself \u2014 its slash commands, MCP servers, sessions, models, keys, or where its files live \u2014 is answered by the `akan-code` skill. Read it rather than describing the tool from memory.",
|
|
1448
|
+
"- 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`."
|
|
1449
|
+
];
|
|
1450
|
+
if (profile.tools.builtin.includes("write"))
|
|
1451
|
+
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.");
|
|
1452
|
+
else
|
|
1453
|
+
lines.push("- This session is read-only: there is no write, edit or bash tool. Report what should change instead of changing it.");
|
|
1454
|
+
if (profile.tools.subagent)
|
|
1455
|
+
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.');
|
|
1456
|
+
if (profile.ui.canPrompt)
|
|
1457
|
+
lines.push("- Ask with `ask_user` when two readings of the request would lead to materially different work, and offer the options you can name. Do not ask for a choice that has an obvious default, and do not ask permission for work you were already asked to do \u2014 make the call, say the assumption, and carry on.");
|
|
1458
|
+
if (profile.approval !== "never")
|
|
1459
|
+
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.`);
|
|
1460
|
+
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.");
|
|
1461
|
+
return lines.join(`
|
|
1462
|
+
`);
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeSessionIndex.ts
|
|
1466
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
|
|
1467
|
+
import path6 from "path";
|
|
1468
|
+
|
|
1469
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/jsonLines.ts
|
|
1470
|
+
import { closeSync, openSync, readFileSync as readFileSync3, readSync, statSync as statSync3 } from "fs";
|
|
1471
|
+
var readHead = (file, maxBytes) => {
|
|
1472
|
+
const fd = openSync(file, "r");
|
|
1473
|
+
try {
|
|
1474
|
+
const head = Buffer.alloc(maxBytes);
|
|
1475
|
+
return head.subarray(0, readSync(fd, head, 0, maxBytes, 0)).toString("utf8");
|
|
1476
|
+
} finally {
|
|
1477
|
+
closeSync(fd);
|
|
1478
|
+
}
|
|
1479
|
+
};
|
|
1480
|
+
var readJsonLines = (file, maxBytes) => {
|
|
1481
|
+
try {
|
|
1482
|
+
const text = maxBytes !== undefined && statSync3(file).size > maxBytes ? readHead(file, maxBytes) : readFileSync3(file, "utf8");
|
|
1483
|
+
return text.split(`
|
|
1484
|
+
`).filter((line) => !!line.trim());
|
|
1485
|
+
} catch {
|
|
1486
|
+
return [];
|
|
1487
|
+
}
|
|
1488
|
+
};
|
|
1489
|
+
var parseJsonLine = (line) => {
|
|
1490
|
+
try {
|
|
1491
|
+
return JSON.parse(line);
|
|
1492
|
+
} catch {
|
|
1493
|
+
return;
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeSessionIndex.ts
|
|
1498
|
+
class CodeSessionIndex2 {
|
|
1499
|
+
static readLimit = 512 * 1024;
|
|
1500
|
+
static list(dir, limit = 30) {
|
|
1501
|
+
if (!existsSync6(dir))
|
|
1502
|
+
return [];
|
|
1503
|
+
const files = readdirSync2(dir).filter((entry) => entry.endsWith(".jsonl")).map((entry) => path6.join(dir, entry)).map((file) => ({ file, updatedAt: CodeSessionIndex2.#mtime(file) })).sort((left, right) => right.updatedAt - left.updatedAt).slice(0, limit);
|
|
1504
|
+
return files.map((entry) => CodeSessionIndex2.#read(entry.file, entry.updatedAt)).filter((entry) => !!entry);
|
|
1505
|
+
}
|
|
1506
|
+
static fileOf(dir, id) {
|
|
1507
|
+
if (!existsSync6(dir))
|
|
1508
|
+
return;
|
|
1509
|
+
const match = readdirSync2(dir).find((entry) => entry.endsWith(`${id}.jsonl`) || entry === `${id}.jsonl`);
|
|
1510
|
+
return match ? path6.join(dir, match) : undefined;
|
|
1511
|
+
}
|
|
1512
|
+
static #mtime(file) {
|
|
1513
|
+
try {
|
|
1514
|
+
return statSync4(file).mtimeMs;
|
|
1515
|
+
} catch {
|
|
1516
|
+
return 0;
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
static #read(file, updatedAt) {
|
|
1520
|
+
const lines = readJsonLines(file, CodeSessionIndex2.readLimit);
|
|
1521
|
+
if (!lines.length)
|
|
1522
|
+
return;
|
|
1523
|
+
const entry = { id: "", file, name: undefined, opening: "", updatedAt, turns: 0 };
|
|
1524
|
+
for (const line of lines) {
|
|
1525
|
+
const record = parseJsonLine(line);
|
|
1526
|
+
if (!record)
|
|
1527
|
+
continue;
|
|
1528
|
+
if (record.type === "session" && typeof record.id === "string")
|
|
1529
|
+
entry.id = record.id;
|
|
1530
|
+
if (record.type === "session_info" && typeof record.name === "string")
|
|
1531
|
+
entry.name = record.name;
|
|
1532
|
+
if (record.type !== "message")
|
|
1533
|
+
continue;
|
|
1534
|
+
const message = record.message;
|
|
1535
|
+
if (message?.role !== "user")
|
|
1536
|
+
continue;
|
|
1537
|
+
entry.turns += 1;
|
|
1538
|
+
if (!entry.opening)
|
|
1539
|
+
entry.opening = CodeSessionIndex2.#text(message.content);
|
|
1540
|
+
}
|
|
1541
|
+
return entry.id ? entry : undefined;
|
|
1542
|
+
}
|
|
1543
|
+
static #text(content) {
|
|
1544
|
+
const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => typeof part === "object" && part && ("text" in part) ? String(part.text) : "").join(" ") : "";
|
|
1545
|
+
return text.replace(/\s+/g, " ").trim();
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/AkanCodeServices.ts
|
|
1550
|
+
class AkanCodeServices2 {
|
|
1551
|
+
static settings() {
|
|
1552
|
+
return SettingsManager.inMemory({
|
|
1553
|
+
enableInstallTelemetry: false,
|
|
1554
|
+
enableAnalytics: false,
|
|
1555
|
+
quietStartup: true,
|
|
1556
|
+
compaction: { reserveTokens: 32768 }
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
static async runtime(workspaceRoot, proxy = null) {
|
|
1560
|
+
mkdirSync(akanCodePaths2.globalDir(), { recursive: true, mode: 448 });
|
|
1561
|
+
const runtime = await ModelRuntime.create({
|
|
1562
|
+
authPath: proxy ? path7.join(mkdtempSync(path7.join(tmpdir(), "akan-code-proxy-")), "auth.json") : akanCodePaths2.authFile(),
|
|
1563
|
+
modelsPath: akanCodePaths2.modelsFile()
|
|
1564
|
+
});
|
|
1565
|
+
if (proxy)
|
|
1566
|
+
await proxy.apply(runtime);
|
|
1567
|
+
else
|
|
1568
|
+
await AkanEnvKeys2.apply(runtime, workspaceRoot);
|
|
1569
|
+
return runtime;
|
|
1570
|
+
}
|
|
1571
|
+
static sessions(workspaceRoot, cwd, profile, resume) {
|
|
1572
|
+
if (profile.session.store === "memory")
|
|
1573
|
+
return SessionManager2.inMemory(cwd);
|
|
1574
|
+
const dir = akanCodePaths2.sessionsDir(workspaceRoot);
|
|
1575
|
+
mkdirSync(dir, { recursive: true });
|
|
1576
|
+
const manager = SessionManager2.create(cwd, dir);
|
|
1577
|
+
if (!resume)
|
|
1578
|
+
return manager;
|
|
1579
|
+
const file = CodeSessionIndex2.fileOf(dir, resume);
|
|
1580
|
+
if (!file)
|
|
1581
|
+
throw new Error(`No stored session ${resume} in ${dir}`);
|
|
1582
|
+
manager.setSessionFile(file);
|
|
1583
|
+
return manager;
|
|
1584
|
+
}
|
|
1585
|
+
static #skillPaths(profile, workspaceRoot) {
|
|
1586
|
+
if (!profile.context.skills)
|
|
1587
|
+
return [];
|
|
1588
|
+
const dirs = [
|
|
1589
|
+
akanCodePaths2.builtinSkillsDir(),
|
|
1590
|
+
akanCodePaths2.globalSkillsDir(),
|
|
1591
|
+
akanCodePaths2.skillsDir(workspaceRoot)
|
|
1592
|
+
];
|
|
1593
|
+
return dirs.filter((dir) => !!dir && existsSync7(dir));
|
|
1594
|
+
}
|
|
1595
|
+
static resourceOptions(options) {
|
|
1596
|
+
const present = (dir) => existsSync7(dir) ? [dir] : [];
|
|
1597
|
+
return {
|
|
1598
|
+
noExtensions: true,
|
|
1599
|
+
noSkills: true,
|
|
1600
|
+
noPromptTemplates: true,
|
|
1601
|
+
noThemes: true,
|
|
1602
|
+
noContextFiles: !options.profile.context.projectFiles,
|
|
1603
|
+
agentsFilesOverride: ({ agentsFiles }) => ({
|
|
1604
|
+
agentsFiles: options.profile.context.projectFiles ? AkanContextFiles.extend(agentsFiles, { workspaceRoot: options.workspaceRoot, cwd: options.cwd }) : agentsFiles
|
|
1605
|
+
}),
|
|
1606
|
+
appendSystemPrompt: [akanSystemPrompt2(options.profile)],
|
|
1607
|
+
additionalExtensionPaths: present(akanCodePaths2.extensionsDir(options.workspaceRoot)),
|
|
1608
|
+
additionalSkillPaths: AkanCodeServices2.#skillPaths(options.profile, options.workspaceRoot),
|
|
1609
|
+
extensionFactories: options.extensions
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentAsks.ts
|
|
1615
|
+
class CodeAgentAsks2 {
|
|
1616
|
+
#question;
|
|
1617
|
+
#approvals = new Map;
|
|
1618
|
+
#nextId = 0;
|
|
1619
|
+
#epoch = Date.now().toString(36);
|
|
1620
|
+
get pendingQuestionId() {
|
|
1621
|
+
return this.#question?.question.questionId;
|
|
1622
|
+
}
|
|
1623
|
+
questionOf(questionId) {
|
|
1624
|
+
return this.#question?.question.questionId === questionId ? this.#question.question : undefined;
|
|
1625
|
+
}
|
|
1626
|
+
get hasPendingApproval() {
|
|
1627
|
+
return this.#approvals.size > 0;
|
|
1628
|
+
}
|
|
1629
|
+
nextId(prefix) {
|
|
1630
|
+
this.#nextId += 1;
|
|
1631
|
+
return `${prefix}${this.#epoch}.${this.#nextId}`;
|
|
1632
|
+
}
|
|
1633
|
+
openQuestion(question) {
|
|
1634
|
+
this.#question?.deferred.resolve("");
|
|
1635
|
+
const deferred = Promise.withResolvers();
|
|
1636
|
+
this.#question = { question, deferred };
|
|
1637
|
+
return deferred.promise;
|
|
1638
|
+
}
|
|
1639
|
+
answer(id, text) {
|
|
1640
|
+
if (this.#question?.question.questionId !== id)
|
|
1641
|
+
return false;
|
|
1642
|
+
const { deferred } = this.#question;
|
|
1643
|
+
this.#question = undefined;
|
|
1644
|
+
deferred.resolve(text);
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
openApproval(id) {
|
|
1648
|
+
const deferred = Promise.withResolvers();
|
|
1649
|
+
this.#approvals.set(id, deferred);
|
|
1650
|
+
return deferred.promise;
|
|
1651
|
+
}
|
|
1652
|
+
resolveApproval(id, approved) {
|
|
1653
|
+
const deferred = this.#approvals.get(id);
|
|
1654
|
+
if (!deferred)
|
|
1655
|
+
return false;
|
|
1656
|
+
this.#approvals.delete(id);
|
|
1657
|
+
deferred.resolve(approved);
|
|
1658
|
+
return true;
|
|
1659
|
+
}
|
|
1660
|
+
clear() {
|
|
1661
|
+
this.#question?.deferred.resolve("");
|
|
1662
|
+
this.#question = undefined;
|
|
1663
|
+
for (const deferred of this.#approvals.values())
|
|
1664
|
+
deferred.resolve(false);
|
|
1665
|
+
this.#approvals.clear();
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentEventMapper.ts
|
|
1670
|
+
import {
|
|
1671
|
+
codeAgentClip as codeAgentClip4,
|
|
1672
|
+
codeAgentLabelChars,
|
|
1673
|
+
codeAgentOutputChars as codeAgentOutputChars4
|
|
1674
|
+
} from "akanjs/common";
|
|
1675
|
+
|
|
1676
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/stringArg.ts
|
|
1677
|
+
var stringArg = (args, key) => {
|
|
1678
|
+
if (!args || typeof args !== "object")
|
|
1679
|
+
return;
|
|
1680
|
+
const value = args[key];
|
|
1681
|
+
return typeof value === "string" && value ? value : undefined;
|
|
1682
|
+
};
|
|
1683
|
+
|
|
1684
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentEventMapper.ts
|
|
1685
|
+
var toolOps = {
|
|
1686
|
+
read: "read",
|
|
1687
|
+
write: "write",
|
|
1688
|
+
edit: "write",
|
|
1689
|
+
ls: "list",
|
|
1690
|
+
find: "search",
|
|
1691
|
+
grep: "search",
|
|
1692
|
+
bash: "execute"
|
|
1693
|
+
};
|
|
1694
|
+
|
|
1695
|
+
class CodeAgentEventMapper2 {
|
|
1696
|
+
#turn = 0;
|
|
1697
|
+
#turnId = "";
|
|
1698
|
+
#outcome;
|
|
1699
|
+
#openTools = new Map;
|
|
1700
|
+
#blocked = new Map;
|
|
1701
|
+
get turnId() {
|
|
1702
|
+
return this.#turnId;
|
|
1703
|
+
}
|
|
1704
|
+
map(event) {
|
|
1705
|
+
switch (event.type) {
|
|
1706
|
+
case "agent_start":
|
|
1707
|
+
this.#turn += 1;
|
|
1708
|
+
this.#turnId = `t${this.#turn}`;
|
|
1709
|
+
this.#outcome = undefined;
|
|
1710
|
+
return [{ type: "turn_start", turnId: this.#turnId }];
|
|
1711
|
+
case "agent_end":
|
|
1712
|
+
return [
|
|
1713
|
+
...this.flushBlocked(),
|
|
1714
|
+
{ type: "turn_end", turnId: this.#turnId, stopReason: this.#stopReason(event) }
|
|
1715
|
+
];
|
|
1716
|
+
case "agent_settled":
|
|
1717
|
+
return [{ type: "idle" }];
|
|
1718
|
+
case "message_update":
|
|
1719
|
+
return this.#mapDelta(event);
|
|
1720
|
+
case "message_end":
|
|
1721
|
+
return this.#mapMessage(event);
|
|
1722
|
+
case "tool_execution_start": {
|
|
1723
|
+
const tool = this.#summary(event.toolCallId, event.toolName, event.args);
|
|
1724
|
+
this.#openTools.set(event.toolCallId, tool);
|
|
1725
|
+
return [{ type: "tool_start", turnId: this.#turnId, tool }];
|
|
1726
|
+
}
|
|
1727
|
+
case "tool_execution_update":
|
|
1728
|
+
return [
|
|
1729
|
+
{
|
|
1730
|
+
type: "tool_progress",
|
|
1731
|
+
turnId: this.#turnId,
|
|
1732
|
+
toolCallId: event.toolCallId,
|
|
1733
|
+
text: codeAgentClip4(CodeAgentEventMapper2.#renderToolText(event.partialResult), codeAgentLabelChars)
|
|
1734
|
+
}
|
|
1735
|
+
];
|
|
1736
|
+
case "tool_execution_end":
|
|
1737
|
+
return [this.#mapToolEnd(event)];
|
|
1738
|
+
case "queue_update":
|
|
1739
|
+
return [{ type: "queue", steering: [...event.steering], followUp: [...event.followUp] }];
|
|
1740
|
+
case "compaction_start":
|
|
1741
|
+
return [{ type: "compaction", phase: "start", reason: event.reason }];
|
|
1742
|
+
case "compaction_end":
|
|
1743
|
+
return [
|
|
1744
|
+
{
|
|
1745
|
+
type: "compaction",
|
|
1746
|
+
phase: "end",
|
|
1747
|
+
reason: event.reason,
|
|
1748
|
+
...event.errorMessage ? { error: event.errorMessage } : {},
|
|
1749
|
+
...event.aborted ? { aborted: true } : {}
|
|
1750
|
+
}
|
|
1751
|
+
];
|
|
1752
|
+
case "auto_retry_start":
|
|
1753
|
+
return [
|
|
1754
|
+
{
|
|
1755
|
+
type: "retry",
|
|
1756
|
+
attempt: event.attempt,
|
|
1757
|
+
maxAttempts: event.maxAttempts,
|
|
1758
|
+
delayMs: event.delayMs,
|
|
1759
|
+
message: event.errorMessage
|
|
1760
|
+
}
|
|
1761
|
+
];
|
|
1762
|
+
default:
|
|
1763
|
+
return [];
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
markBlocked(toolCallId, toolName, args, reason) {
|
|
1767
|
+
this.#blocked.set(toolCallId, reason);
|
|
1768
|
+
if (this.#openTools.has(toolCallId))
|
|
1769
|
+
return [];
|
|
1770
|
+
const tool = this.#summary(toolCallId, toolName, args);
|
|
1771
|
+
this.#openTools.set(toolCallId, tool);
|
|
1772
|
+
return [{ type: "tool_start", turnId: this.#turnId, tool }];
|
|
1773
|
+
}
|
|
1774
|
+
flushBlocked() {
|
|
1775
|
+
const frames = [];
|
|
1776
|
+
for (const [toolCallId, reason] of this.#blocked) {
|
|
1777
|
+
const tool = this.#openTools.get(toolCallId);
|
|
1778
|
+
if (!tool)
|
|
1779
|
+
continue;
|
|
1780
|
+
this.#openTools.delete(toolCallId);
|
|
1781
|
+
frames.push({
|
|
1782
|
+
type: "tool_end",
|
|
1783
|
+
turnId: this.#turnId,
|
|
1784
|
+
tool,
|
|
1785
|
+
outcome: "blocked",
|
|
1786
|
+
output: reason,
|
|
1787
|
+
truncated: false
|
|
1788
|
+
});
|
|
1789
|
+
}
|
|
1790
|
+
this.#blocked.clear();
|
|
1791
|
+
return frames;
|
|
1792
|
+
}
|
|
1793
|
+
#mapToolEnd(event) {
|
|
1794
|
+
const tool = this.#openTools.get(event.toolCallId) ?? this.#summary(event.toolCallId, event.toolName, undefined);
|
|
1795
|
+
this.#openTools.delete(event.toolCallId);
|
|
1796
|
+
const reason = this.#blocked.get(event.toolCallId);
|
|
1797
|
+
this.#blocked.delete(event.toolCallId);
|
|
1798
|
+
const full = reason ?? CodeAgentEventMapper2.#renderToolText(event.result);
|
|
1799
|
+
return {
|
|
1800
|
+
type: "tool_end",
|
|
1801
|
+
turnId: this.#turnId,
|
|
1802
|
+
tool,
|
|
1803
|
+
outcome: reason ? "blocked" : event.isError ? "error" : "ok",
|
|
1804
|
+
output: codeAgentClip4(full, codeAgentOutputChars4),
|
|
1805
|
+
truncated: full.length > codeAgentOutputChars4
|
|
1806
|
+
};
|
|
1807
|
+
}
|
|
1808
|
+
noteOutcome(reason) {
|
|
1809
|
+
this.#outcome = reason;
|
|
1810
|
+
}
|
|
1811
|
+
#stopReason(event) {
|
|
1812
|
+
const noted = this.#outcome;
|
|
1813
|
+
this.#outcome = undefined;
|
|
1814
|
+
if (noted)
|
|
1815
|
+
return noted;
|
|
1816
|
+
if (event.willRetry)
|
|
1817
|
+
return "error";
|
|
1818
|
+
const last = event.messages.at(-1);
|
|
1819
|
+
if (last?.stopReason === "aborted")
|
|
1820
|
+
return "aborted";
|
|
1821
|
+
if (last?.stopReason === "error")
|
|
1822
|
+
return "error";
|
|
1823
|
+
if (last?.stopReason === "length")
|
|
1824
|
+
return "truncated";
|
|
1825
|
+
return "done";
|
|
1826
|
+
}
|
|
1827
|
+
#summary(toolCallId, name, args) {
|
|
1828
|
+
const path = stringArg(args, "path");
|
|
1829
|
+
return {
|
|
1830
|
+
toolCallId,
|
|
1831
|
+
name,
|
|
1832
|
+
op: toolOps[name] ?? "other",
|
|
1833
|
+
...path ? { path } : {},
|
|
1834
|
+
title: codeAgentClip4(`${name}${CodeAgentEventMapper2.#renderArgs(args)}`, codeAgentLabelChars)
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
#mapDelta(event) {
|
|
1838
|
+
const inner = event.assistantMessageEvent;
|
|
1839
|
+
if (inner.type === "text_delta")
|
|
1840
|
+
return [{ type: "text_delta", turnId: this.#turnId, text: inner.delta }];
|
|
1841
|
+
if (inner.type === "thinking_delta")
|
|
1842
|
+
return [{ type: "thinking_delta", turnId: this.#turnId, text: inner.delta }];
|
|
1843
|
+
return [];
|
|
1844
|
+
}
|
|
1845
|
+
#mapMessage(event) {
|
|
1846
|
+
const message = event.message;
|
|
1847
|
+
if (message.role !== "assistant" && message.role !== "user")
|
|
1848
|
+
return [];
|
|
1849
|
+
const text = CodeAgentEventMapper2.#renderContent(message.content);
|
|
1850
|
+
if (!text)
|
|
1851
|
+
return [];
|
|
1852
|
+
return [{ type: "message", turnId: this.#turnId, role: message.role, text }];
|
|
1853
|
+
}
|
|
1854
|
+
static #renderArgs(args) {
|
|
1855
|
+
if (!args || typeof args !== "object")
|
|
1856
|
+
return "";
|
|
1857
|
+
const entries = Object.entries(args).filter(([, value]) => value !== undefined);
|
|
1858
|
+
if (!entries.length)
|
|
1859
|
+
return "";
|
|
1860
|
+
const rendered = entries.map(([key, value]) => `${key}=${CodeAgentEventMapper2.#renderArgValue(value)}`).join(" ");
|
|
1861
|
+
return `(${codeAgentClip4(rendered, 120)})`;
|
|
1862
|
+
}
|
|
1863
|
+
static #renderArgValue(value) {
|
|
1864
|
+
if (Array.isArray(value))
|
|
1865
|
+
return `${value.length} item${value.length === 1 ? "" : "s"}`;
|
|
1866
|
+
if (value && typeof value === "object") {
|
|
1867
|
+
const keys = Object.keys(value).length;
|
|
1868
|
+
return `{${keys} key${keys === 1 ? "" : "s"}}`;
|
|
1869
|
+
}
|
|
1870
|
+
return codeAgentClip4(String(value), 60);
|
|
1871
|
+
}
|
|
1872
|
+
static #renderContent(content) {
|
|
1873
|
+
if (typeof content === "string")
|
|
1874
|
+
return content;
|
|
1875
|
+
if (!Array.isArray(content))
|
|
1876
|
+
return "";
|
|
1877
|
+
return content.filter((part) => !!part && typeof part === "object" && part.type === "text").map((part) => part.text).join("");
|
|
1878
|
+
}
|
|
1879
|
+
static #renderToolText(value) {
|
|
1880
|
+
if (typeof value === "string")
|
|
1881
|
+
return value;
|
|
1882
|
+
if (value && typeof value === "object" && "content" in value)
|
|
1883
|
+
return CodeAgentEventMapper2.#renderContent(value.content);
|
|
1884
|
+
return value === undefined ? "" : JSON.stringify(value);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1888
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentGate.ts
|
|
1889
|
+
import path8 from "path";
|
|
1890
|
+
var writeTools = new Set(["write", "edit"]);
|
|
1891
|
+
var commandTools = new Set(["bash"]);
|
|
1892
|
+
|
|
1893
|
+
class CodeAgentGate2 {
|
|
1894
|
+
#profile;
|
|
1895
|
+
#deny;
|
|
1896
|
+
#allow;
|
|
1897
|
+
#deniedFragments;
|
|
1898
|
+
constructor(profile) {
|
|
1899
|
+
this.#profile = profile;
|
|
1900
|
+
this.#deny = (profile.paths.deny ?? []).map((pattern) => new Bun.Glob(pattern));
|
|
1901
|
+
this.#allow = (profile.paths.allow ?? []).map((pattern) => new Bun.Glob(pattern));
|
|
1902
|
+
this.#deniedFragments = [...new Set((profile.paths.deny ?? []).map(CodeAgentGate2.#fragmentOf).filter(Boolean))];
|
|
1903
|
+
}
|
|
1904
|
+
verdict(toolName, args) {
|
|
1905
|
+
const target = stringArg(args, "path");
|
|
1906
|
+
if (target) {
|
|
1907
|
+
const denied = this.#deniedReason(target);
|
|
1908
|
+
if (denied)
|
|
1909
|
+
return { block: denied };
|
|
1910
|
+
}
|
|
1911
|
+
const command = stringArg(args, "command");
|
|
1912
|
+
if (command) {
|
|
1913
|
+
const denied = this.#deniedCommandReason(command);
|
|
1914
|
+
if (denied)
|
|
1915
|
+
return { block: denied };
|
|
1916
|
+
}
|
|
1917
|
+
if (!this.#needsApproval(toolName))
|
|
1918
|
+
return {};
|
|
1919
|
+
return { approval: CodeAgentGate2.#summarize(toolName, args) };
|
|
1920
|
+
}
|
|
1921
|
+
#deniedCommandReason(command) {
|
|
1922
|
+
const hit = this.#deniedFragments.find((fragment) => command.includes(fragment));
|
|
1923
|
+
if (!hit)
|
|
1924
|
+
return;
|
|
1925
|
+
return `The command names "${hit}", which the profile's deny list covers. Ask the user for the value instead of reading the file.`;
|
|
1926
|
+
}
|
|
1927
|
+
#deniedReason(target) {
|
|
1928
|
+
const absolute = path8.isAbsolute(target) ? target : path8.resolve(this.#profile.paths.root, target);
|
|
1929
|
+
const relative = path8.relative(this.#profile.paths.root, absolute);
|
|
1930
|
+
if (relative.startsWith("..") || path8.isAbsolute(relative))
|
|
1931
|
+
return `Path is outside the agent root (${this.#profile.paths.root}).`;
|
|
1932
|
+
if (this.#deny.some((glob) => glob.match(absolute) || glob.match(relative)))
|
|
1933
|
+
return "Path is on the profile's deny list (secrets and environment files are never readable).";
|
|
1934
|
+
if (this.#allow.length && !this.#allow.some((glob) => glob.match(absolute) || glob.match(relative)))
|
|
1935
|
+
return "Path is outside the profile's allow list.";
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
#needsApproval(toolName) {
|
|
1939
|
+
switch (this.#profile.approval) {
|
|
1940
|
+
case "never":
|
|
1941
|
+
return false;
|
|
1942
|
+
case "writes":
|
|
1943
|
+
return writeTools.has(toolName);
|
|
1944
|
+
case "commands":
|
|
1945
|
+
return commandTools.has(toolName);
|
|
1946
|
+
case "all":
|
|
1947
|
+
return true;
|
|
1948
|
+
default:
|
|
1949
|
+
return false;
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
static #fragmentOf(pattern) {
|
|
1953
|
+
const trimmed = pattern.replace(/^\*\*\//, "").replace(/\/\*\*$/, "/").replace(/^\*+/, "");
|
|
1954
|
+
return trimmed.includes("*") ? "" : trimmed;
|
|
1955
|
+
}
|
|
1956
|
+
static #summarize(toolName, args) {
|
|
1957
|
+
const record = args ?? {};
|
|
1958
|
+
if (toolName === "bash")
|
|
1959
|
+
return `run: ${String(record.command ?? "").slice(0, 200)}`;
|
|
1960
|
+
const target = stringArg(args, "path");
|
|
1961
|
+
return target ? `${toolName} ${target}` : toolName;
|
|
1962
|
+
}
|
|
1963
|
+
}
|
|
1964
|
+
|
|
1965
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentProxy.ts
|
|
1966
|
+
import { readFile } from "fs/promises";
|
|
1967
|
+
class CodeAgentProxy {
|
|
1968
|
+
static placeholderKey = "akan-code-proxy";
|
|
1969
|
+
baseUrl;
|
|
1970
|
+
providers;
|
|
1971
|
+
#tokenFile;
|
|
1972
|
+
#token;
|
|
1973
|
+
constructor({ baseUrl, providers, tokenFile, token }) {
|
|
1974
|
+
this.baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1975
|
+
this.providers = providers;
|
|
1976
|
+
this.#tokenFile = tokenFile;
|
|
1977
|
+
this.#token = token;
|
|
1978
|
+
if (!tokenFile && !token)
|
|
1979
|
+
throw new Error("AKAN_CODE_PROXY_URL needs AKAN_CODE_PROXY_TOKEN_FILE or AKAN_CODE_PROXY_TOKEN");
|
|
1980
|
+
}
|
|
1981
|
+
static fromEnv(profile) {
|
|
1982
|
+
const baseUrl = process.env.AKAN_CODE_PROXY_URL?.trim() || profile?.network.proxyBaseUrl;
|
|
1983
|
+
if (!baseUrl)
|
|
1984
|
+
return null;
|
|
1985
|
+
const providers = (process.env.AKAN_CODE_PROXY_PROVIDERS ?? akanCodeDefaultModel2.provider).split(",").map((provider) => provider.trim()).filter(Boolean);
|
|
1986
|
+
return new CodeAgentProxy({
|
|
1987
|
+
baseUrl,
|
|
1988
|
+
providers,
|
|
1989
|
+
tokenFile: process.env.AKAN_CODE_PROXY_TOKEN_FILE?.trim() || undefined,
|
|
1990
|
+
token: process.env.AKAN_CODE_PROXY_TOKEN?.trim() || undefined
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
baseUrlFor(provider) {
|
|
1994
|
+
return this.baseUrl.replaceAll("{provider}", provider);
|
|
1995
|
+
}
|
|
1996
|
+
async token() {
|
|
1997
|
+
if (!this.#tokenFile)
|
|
1998
|
+
return this.#token ?? "";
|
|
1999
|
+
const token = (await readFile(this.#tokenFile, "utf8")).trim();
|
|
2000
|
+
if (!token)
|
|
2001
|
+
throw new Error(`${this.#tokenFile} is empty`);
|
|
2002
|
+
return token;
|
|
2003
|
+
}
|
|
2004
|
+
async apply(runtime) {
|
|
2005
|
+
const target = runtime;
|
|
2006
|
+
const original = target.prepareRequest?.bind(runtime);
|
|
2007
|
+
if (!original)
|
|
2008
|
+
throw new Error("The model runtime no longer exposes prepareRequest; update CodeAgentProxy");
|
|
2009
|
+
for (const provider of this.providers)
|
|
2010
|
+
await runtime.setRuntimeApiKey(provider, CodeAgentProxy.placeholderKey);
|
|
2011
|
+
target.prepareRequest = async (model, options = {}) => {
|
|
2012
|
+
const apiKey = await this.token();
|
|
2013
|
+
const prepared = await original(model, { ...options, apiKey });
|
|
2014
|
+
return {
|
|
2015
|
+
...prepared,
|
|
2016
|
+
model: { ...prepared.model, baseUrl: this.baseUrlFor(prepared.model.provider) },
|
|
2017
|
+
options: { ...prepared.options, apiKey }
|
|
2018
|
+
};
|
|
2019
|
+
};
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
2023
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentSuspended.ts
|
|
2024
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync2, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
|
|
2025
|
+
import path9 from "path";
|
|
2026
|
+
|
|
2027
|
+
class CodeAgentSuspended {
|
|
2028
|
+
#file;
|
|
2029
|
+
#state;
|
|
2030
|
+
constructor(file) {
|
|
2031
|
+
this.#file = file;
|
|
2032
|
+
this.#state = CodeAgentSuspended.#read(file);
|
|
2033
|
+
}
|
|
2034
|
+
static fileOf(sessionDir, sessionId) {
|
|
2035
|
+
return sessionDir ? path9.join(sessionDir, `${sessionId}.suspended.json`) : null;
|
|
2036
|
+
}
|
|
2037
|
+
get question() {
|
|
2038
|
+
return this.#state.question;
|
|
2039
|
+
}
|
|
2040
|
+
get approvals() {
|
|
2041
|
+
return this.#state.approvals;
|
|
2042
|
+
}
|
|
2043
|
+
setQuestion(question) {
|
|
2044
|
+
this.#state = { ...this.#state, question };
|
|
2045
|
+
this.#save();
|
|
2046
|
+
}
|
|
2047
|
+
addApproval(request) {
|
|
2048
|
+
this.#state = { ...this.#state, approvals: [...this.#state.approvals, request] };
|
|
2049
|
+
this.#save();
|
|
2050
|
+
}
|
|
2051
|
+
takeQuestion(questionId) {
|
|
2052
|
+
const question = this.#state.question;
|
|
2053
|
+
if (question?.questionId !== questionId)
|
|
2054
|
+
return;
|
|
2055
|
+
this.#state = { approvals: this.#state.approvals };
|
|
2056
|
+
this.#save();
|
|
2057
|
+
return question;
|
|
2058
|
+
}
|
|
2059
|
+
takeApproval(approvalId) {
|
|
2060
|
+
const request = this.#state.approvals.find((approval) => approval.approvalId === approvalId);
|
|
2061
|
+
if (!request)
|
|
2062
|
+
return;
|
|
2063
|
+
this.#state = { ...this.#state, approvals: this.#state.approvals.filter((approval) => approval !== request) };
|
|
2064
|
+
this.#save();
|
|
2065
|
+
return request;
|
|
2066
|
+
}
|
|
2067
|
+
clear() {
|
|
2068
|
+
if (!this.#state.question && !this.#state.approvals.length)
|
|
2069
|
+
return;
|
|
2070
|
+
this.#state = { approvals: [] };
|
|
2071
|
+
this.#save();
|
|
2072
|
+
}
|
|
2073
|
+
#save() {
|
|
2074
|
+
if (!this.#file)
|
|
2075
|
+
return;
|
|
2076
|
+
if (!this.#state.question && !this.#state.approvals.length) {
|
|
2077
|
+
rmSync(this.#file, { force: true });
|
|
2078
|
+
return;
|
|
2079
|
+
}
|
|
2080
|
+
mkdirSync2(path9.dirname(this.#file), { recursive: true });
|
|
2081
|
+
writeFileSync(this.#file, `${JSON.stringify(this.#state)}
|
|
2082
|
+
`);
|
|
2083
|
+
}
|
|
2084
|
+
static #read(file) {
|
|
2085
|
+
if (!file || !existsSync8(file))
|
|
2086
|
+
return { approvals: [] };
|
|
2087
|
+
try {
|
|
2088
|
+
const parsed = JSON.parse(readFileSync4(file, "utf8"));
|
|
2089
|
+
return { ...parsed.question ? { question: parsed.question } : {}, approvals: parsed.approvals ?? [] };
|
|
2090
|
+
} catch {
|
|
2091
|
+
return { approvals: [] };
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgentUi.ts
|
|
2097
|
+
class CodeAgentUi2 {
|
|
2098
|
+
#handlers;
|
|
2099
|
+
constructor(handlers) {
|
|
2100
|
+
this.#handlers = handlers;
|
|
2101
|
+
}
|
|
2102
|
+
context() {
|
|
2103
|
+
const noop = () => {};
|
|
2104
|
+
const ui = {
|
|
2105
|
+
select: async (title, options) => await this.#handlers.ask(title, "select", options),
|
|
2106
|
+
confirm: async (title, message) => {
|
|
2107
|
+
const answer = await this.#handlers.ask(message ? `${title}: ${message}` : title, "confirm");
|
|
2108
|
+
return answer === "yes" || answer === "true";
|
|
2109
|
+
},
|
|
2110
|
+
input: async (title) => await this.#handlers.ask(title, "text"),
|
|
2111
|
+
notify: (message, type = "info") => this.#handlers.notify(type, message),
|
|
2112
|
+
editor: async (title, prefill) => await this.#handlers.ask(title, "text") ?? prefill,
|
|
2113
|
+
onTerminalInput: () => noop,
|
|
2114
|
+
setStatus: noop,
|
|
2115
|
+
setWorkingMessage: noop,
|
|
2116
|
+
setWorkingVisible: noop,
|
|
2117
|
+
setWorkingIndicator: noop,
|
|
2118
|
+
setHiddenThinkingLabel: noop,
|
|
2119
|
+
setWidget: noop,
|
|
2120
|
+
setFooter: noop,
|
|
2121
|
+
setHeader: noop,
|
|
2122
|
+
setTitle: noop,
|
|
2123
|
+
custom: async () => {
|
|
2124
|
+
return;
|
|
2125
|
+
},
|
|
2126
|
+
pasteToEditor: noop,
|
|
2127
|
+
setEditorText: noop,
|
|
2128
|
+
getEditorText: () => "",
|
|
2129
|
+
addAutocompleteProvider: noop,
|
|
2130
|
+
setCustomEditorComponent: noop
|
|
2131
|
+
};
|
|
2132
|
+
return ui;
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2135
|
+
|
|
2136
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeMailbox.ts
|
|
2137
|
+
import {
|
|
2138
|
+
appendFileSync,
|
|
2139
|
+
existsSync as existsSync9,
|
|
2140
|
+
mkdirSync as mkdirSync3,
|
|
2141
|
+
readdirSync as readdirSync3,
|
|
2142
|
+
readFileSync as readFileSync5,
|
|
2143
|
+
rmSync as rmSync2,
|
|
2144
|
+
statSync as statSync5,
|
|
2145
|
+
writeFileSync as writeFileSync2
|
|
2146
|
+
} from "fs";
|
|
2147
|
+
import path10 from "path";
|
|
2148
|
+
class CodeMailbox2 {
|
|
2149
|
+
static staleMs = 20000;
|
|
2150
|
+
static beatMs = 5000;
|
|
2151
|
+
static pollMs = 1500;
|
|
2152
|
+
static sendsPerMinute = 20;
|
|
2153
|
+
#dir;
|
|
2154
|
+
#id;
|
|
2155
|
+
#name;
|
|
2156
|
+
#read = 0;
|
|
2157
|
+
#sent = [];
|
|
2158
|
+
#beat = null;
|
|
2159
|
+
#poll = null;
|
|
2160
|
+
constructor(dir, id, name) {
|
|
2161
|
+
this.#dir = dir;
|
|
2162
|
+
this.#id = id;
|
|
2163
|
+
this.#name = name;
|
|
2164
|
+
}
|
|
2165
|
+
get inbox() {
|
|
2166
|
+
return path10.join(this.#dir, "inbox", `${this.#id}.jsonl`);
|
|
2167
|
+
}
|
|
2168
|
+
open(cwd, onMail) {
|
|
2169
|
+
mkdirSync3(path10.join(this.#dir, "live"), { recursive: true });
|
|
2170
|
+
mkdirSync3(path10.join(this.#dir, "inbox"), { recursive: true });
|
|
2171
|
+
this.#read = readJsonLines(this.inbox).length;
|
|
2172
|
+
this.beat(cwd);
|
|
2173
|
+
this.#beat = setInterval(() => this.beat(cwd), CodeMailbox2.beatMs);
|
|
2174
|
+
this.#poll = setInterval(() => {
|
|
2175
|
+
for (const mail of this.#drain())
|
|
2176
|
+
onMail(mail);
|
|
2177
|
+
}, CodeMailbox2.pollMs);
|
|
2178
|
+
}
|
|
2179
|
+
close() {
|
|
2180
|
+
if (this.#beat)
|
|
2181
|
+
clearInterval(this.#beat);
|
|
2182
|
+
if (this.#poll)
|
|
2183
|
+
clearInterval(this.#poll);
|
|
2184
|
+
this.#beat = null;
|
|
2185
|
+
this.#poll = null;
|
|
2186
|
+
try {
|
|
2187
|
+
rmSync2(path10.join(this.#dir, "live", `${this.#id}.json`), { force: true });
|
|
2188
|
+
} catch {}
|
|
2189
|
+
}
|
|
2190
|
+
rename(name, cwd) {
|
|
2191
|
+
this.#name = name;
|
|
2192
|
+
this.beat(cwd);
|
|
2193
|
+
}
|
|
2194
|
+
beat(cwd) {
|
|
2195
|
+
const peer = { id: this.#id, name: this.#name, cwd, pid: process.pid, at: Date.now() };
|
|
2196
|
+
try {
|
|
2197
|
+
writeFileSync2(path10.join(this.#dir, "live", `${this.#id}.json`), JSON.stringify(peer));
|
|
2198
|
+
} catch {}
|
|
2199
|
+
}
|
|
2200
|
+
peers() {
|
|
2201
|
+
const dir = path10.join(this.#dir, "live");
|
|
2202
|
+
if (!existsSync9(dir))
|
|
2203
|
+
return [];
|
|
2204
|
+
const now = Date.now();
|
|
2205
|
+
return readdirSync3(dir).filter((entry) => entry.endsWith(".json")).map((entry) => CodeMailbox2.#peer(path10.join(dir, entry))).filter((peer) => !!peer && peer.id !== this.#id && now - peer.at < CodeMailbox2.staleMs).sort((left, right) => right.at - left.at);
|
|
2206
|
+
}
|
|
2207
|
+
find(target) {
|
|
2208
|
+
const needle = target.trim().toLowerCase();
|
|
2209
|
+
const peers = this.peers();
|
|
2210
|
+
const byName = peers.filter((peer) => peer.name.toLowerCase() === needle);
|
|
2211
|
+
if (byName.length === 1)
|
|
2212
|
+
return byName[0];
|
|
2213
|
+
const byPrefix = peers.filter((peer) => peer.id.startsWith(needle) || peer.name.toLowerCase().startsWith(needle));
|
|
2214
|
+
return byPrefix.length === 1 ? byPrefix[0] : undefined;
|
|
2215
|
+
}
|
|
2216
|
+
send(to, text) {
|
|
2217
|
+
const now = Date.now();
|
|
2218
|
+
this.#sent = this.#sent.filter((at) => now - at < 60000);
|
|
2219
|
+
if (this.#sent.length >= CodeMailbox2.sendsPerMinute)
|
|
2220
|
+
throw new Error(`Too many messages: ${CodeMailbox2.sendsPerMinute} a minute is the ceiling.`);
|
|
2221
|
+
this.#sent.push(now);
|
|
2222
|
+
const mail = { at: now, from: this.#id, fromName: this.#name, text };
|
|
2223
|
+
mkdirSync3(path10.join(this.#dir, "inbox"), { recursive: true });
|
|
2224
|
+
appendFileSync(path10.join(this.#dir, "inbox", `${to}.jsonl`), `${JSON.stringify(mail)}
|
|
2225
|
+
`);
|
|
2226
|
+
}
|
|
2227
|
+
#drain() {
|
|
2228
|
+
const lines = readJsonLines(this.inbox);
|
|
2229
|
+
if (lines.length <= this.#read)
|
|
2230
|
+
return [];
|
|
2231
|
+
const fresh = lines.slice(this.#read);
|
|
2232
|
+
this.#read = lines.length;
|
|
2233
|
+
return fresh.map((line) => parseJsonLine(line)).filter((mail) => !!mail?.text && typeof mail.from === "string");
|
|
2234
|
+
}
|
|
2235
|
+
static #peer(file) {
|
|
2236
|
+
try {
|
|
2237
|
+
const peer = JSON.parse(readFileSync5(file, "utf8"));
|
|
2238
|
+
return typeof peer.id === "string" ? { ...peer, at: peer.at || statSync5(file).mtimeMs } : undefined;
|
|
2239
|
+
} catch {
|
|
2240
|
+
return;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
|
|
2245
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeSessionFork.ts
|
|
2246
|
+
import { randomBytes } from "crypto";
|
|
2247
|
+
import { readFileSync as readFileSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
2248
|
+
import path11 from "path";
|
|
2249
|
+
import { codeAgentSessionName } from "akanjs/common";
|
|
2250
|
+
class CodeSessionFork2 {
|
|
2251
|
+
static fork(dir, id, name) {
|
|
2252
|
+
const source = CodeSessionIndex2.fileOf(dir, id);
|
|
2253
|
+
if (!source)
|
|
2254
|
+
throw new Error(`No stored session ${id} in ${dir}`);
|
|
2255
|
+
const lines = readFileSync6(source, "utf8").split(`
|
|
2256
|
+
`).filter((line) => !!line.trim());
|
|
2257
|
+
if (!lines.length)
|
|
2258
|
+
throw new Error(`Session ${id} has nothing in it to fork`);
|
|
2259
|
+
const at = new Date;
|
|
2260
|
+
const forked = Bun.randomUUIDv7();
|
|
2261
|
+
const records = lines.map((line) => parseJsonLine(line)).filter((record) => !!record);
|
|
2262
|
+
const first = records.find((record) => record.type === "session");
|
|
2263
|
+
if (!first)
|
|
2264
|
+
throw new Error(`Session ${id} carries no session record`);
|
|
2265
|
+
first.id = forked;
|
|
2266
|
+
first.timestamp = at.toISOString();
|
|
2267
|
+
records.push(CodeSessionFork2.#named(records, name ?? CodeSessionFork2.#nameOf(records), at));
|
|
2268
|
+
const file = path11.join(dir, `${at.toISOString().replace(/[:.]/g, "-")}_${forked}.jsonl`);
|
|
2269
|
+
writeFileSync3(file, `${records.map((record) => JSON.stringify(record)).join(`
|
|
2270
|
+
`)}
|
|
2271
|
+
`);
|
|
2272
|
+
return { id: forked, file };
|
|
2273
|
+
}
|
|
2274
|
+
static #named(records, name, at) {
|
|
2275
|
+
const parent = records.at(-1)?.id;
|
|
2276
|
+
return {
|
|
2277
|
+
type: "session_info",
|
|
2278
|
+
id: randomBytes(4).toString("hex"),
|
|
2279
|
+
parentId: typeof parent === "string" ? parent : null,
|
|
2280
|
+
timestamp: at.toISOString(),
|
|
2281
|
+
name
|
|
2282
|
+
};
|
|
2283
|
+
}
|
|
2284
|
+
static #nameOf(records) {
|
|
2285
|
+
const named = records.filter((record) => record.type === "session_info").at(-1);
|
|
2286
|
+
const current = typeof named?.name === "string" ? named.name : "";
|
|
2287
|
+
return codeAgentSessionName(current ? `${current} fork` : "fork");
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
2291
|
+
// pkgs/@akanjs/devkit/codeAgent/agent/CodeAgent.ts
|
|
2292
|
+
class CodeAgent2 {
|
|
2293
|
+
#profile;
|
|
2294
|
+
#gate;
|
|
2295
|
+
#mapper = new CodeAgentEventMapper2;
|
|
2296
|
+
#asks = new CodeAgentAsks2;
|
|
2297
|
+
#suspended = new CodeAgentSuspended(null);
|
|
2298
|
+
#listeners = new Set;
|
|
2299
|
+
#session;
|
|
2300
|
+
#disposePlugins;
|
|
2301
|
+
#seq = 0;
|
|
2302
|
+
#disposed = false;
|
|
2303
|
+
#sessionDir;
|
|
2304
|
+
#mailbox;
|
|
2305
|
+
#mcp = [];
|
|
2306
|
+
#workspaceRoot = "";
|
|
2307
|
+
#replay = [];
|
|
2308
|
+
#replayFrom = 0;
|
|
2309
|
+
#lastContextTokens;
|
|
2310
|
+
constructor(profile) {
|
|
2311
|
+
this.#profile = profile;
|
|
2312
|
+
this.#gate = new CodeAgentGate2(profile);
|
|
2313
|
+
}
|
|
2314
|
+
static async create(options) {
|
|
2315
|
+
const workspaceRoot = options.workspace.workspaceRoot;
|
|
2316
|
+
const cwd = options.cwd ?? workspaceRoot;
|
|
2317
|
+
const agent = new CodeAgent2(options.profile);
|
|
2318
|
+
const settingsManager = AkanCodeServices2.settings();
|
|
2319
|
+
const modelRuntime = await AkanCodeServices2.runtime(workspaceRoot, CodeAgentProxy.fromEnv(options.profile));
|
|
2320
|
+
const model = await akanCodeModel2(modelRuntime, options.model);
|
|
2321
|
+
const compaction = settingsManager.getCompactionSettings(model);
|
|
2322
|
+
const modelWarnings = akanCodeModelWarnings2(model, compaction.reserveTokens + compaction.keepRecentTokens);
|
|
2323
|
+
const akan = await AkanCodePlugins2.build({
|
|
2324
|
+
workspace: options.workspace,
|
|
2325
|
+
cwd,
|
|
2326
|
+
profile: options.profile,
|
|
2327
|
+
apps: options.apps ?? [],
|
|
2328
|
+
canSeeImages: akanCodeModelSupportsImages2(model),
|
|
2329
|
+
depth: options.depth ?? 0,
|
|
2330
|
+
...options.subagentSpend ? { subagentSpend: options.subagentSpend } : {},
|
|
2331
|
+
currentSessionId: () => agent.sessionId,
|
|
2332
|
+
mailbox: () => agent.#mailbox,
|
|
2333
|
+
onNotice: (message) => agent.#emit({ type: "notice", level: "warning", message }),
|
|
2334
|
+
onAgents: (agents) => agent.#emit({ type: "subagent", agents }),
|
|
2335
|
+
onAsk: (question) => agent.ask(question)
|
|
2336
|
+
});
|
|
2337
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
2338
|
+
cwd,
|
|
2339
|
+
agentDir: akanCodePaths2.globalDir(),
|
|
2340
|
+
settingsManager,
|
|
2341
|
+
...AkanCodeServices2.resourceOptions({
|
|
2342
|
+
workspaceRoot,
|
|
2343
|
+
cwd,
|
|
2344
|
+
profile: options.profile,
|
|
2345
|
+
extensions: [agent.#gateExtension(), ...akan.extensions, ...options.extensions ?? []]
|
|
2346
|
+
})
|
|
2347
|
+
});
|
|
2348
|
+
await resourceLoader.reload();
|
|
2349
|
+
const { session } = await createAgentSession({
|
|
2350
|
+
cwd,
|
|
2351
|
+
agentDir: akanCodePaths2.globalDir(),
|
|
2352
|
+
modelRuntime,
|
|
2353
|
+
settingsManager,
|
|
2354
|
+
resourceLoader,
|
|
2355
|
+
model,
|
|
2356
|
+
sessionManager: AkanCodeServices2.sessions(workspaceRoot, cwd, options.profile, options.resume),
|
|
2357
|
+
noTools: "builtin",
|
|
2358
|
+
tools: [...options.profile.tools.builtin, ...akan.toolNames],
|
|
2359
|
+
customTools: options.customTools
|
|
2360
|
+
});
|
|
2361
|
+
agent.#disposePlugins = akan.dispose;
|
|
2362
|
+
agent.#mcp = akan.mcp;
|
|
2363
|
+
agent.#workspaceRoot = workspaceRoot;
|
|
2364
|
+
if (options.profile.session.store !== "memory")
|
|
2365
|
+
agent.#sessionDir = akanCodePaths2.sessionsDir(workspaceRoot);
|
|
2366
|
+
await agent.#attach(session, options.mode ?? "print");
|
|
2367
|
+
agent.#suspended = new CodeAgentSuspended(CodeAgentSuspended.fileOf(agent.#sessionDir, agent.sessionId));
|
|
2368
|
+
if (agent.#sessionDir && options.profile.session.store === "file")
|
|
2369
|
+
agent.#openMail(akanCodePaths2.mailDir(workspaceRoot), cwd);
|
|
2370
|
+
for (const message of modelWarnings)
|
|
2371
|
+
agent.#emit({ type: "notice", level: "warning", message });
|
|
2372
|
+
return agent;
|
|
2373
|
+
}
|
|
2374
|
+
get profile() {
|
|
2375
|
+
return this.#profile;
|
|
2376
|
+
}
|
|
2377
|
+
get canSeeImages() {
|
|
2378
|
+
const model = this.#session?.model;
|
|
2379
|
+
return model ? akanCodeModelSupportsImages2(model) : false;
|
|
2380
|
+
}
|
|
2381
|
+
sessions() {
|
|
2382
|
+
return this.#sessionDir ? CodeSessionIndex2.list(this.#sessionDir) : [];
|
|
2383
|
+
}
|
|
2384
|
+
get workspaceRoot() {
|
|
2385
|
+
return this.#workspaceRoot;
|
|
2386
|
+
}
|
|
2387
|
+
mcpServers() {
|
|
2388
|
+
return this.#mcp;
|
|
2389
|
+
}
|
|
2390
|
+
peers() {
|
|
2391
|
+
return this.#mailbox?.peers() ?? [];
|
|
2392
|
+
}
|
|
2393
|
+
send(target, text) {
|
|
2394
|
+
const mailbox = this.#mailbox;
|
|
2395
|
+
if (!mailbox)
|
|
2396
|
+
throw new Error("This profile keeps no shared mailbox, so it has no peers.");
|
|
2397
|
+
const peer = mailbox.find(target);
|
|
2398
|
+
if (!peer)
|
|
2399
|
+
throw new Error(`No live session matches "${target}". Ask for the list first.`);
|
|
2400
|
+
mailbox.send(peer.id, text);
|
|
2401
|
+
return peer;
|
|
2402
|
+
}
|
|
2403
|
+
#openMail(dir, cwd) {
|
|
2404
|
+
const session = this.#session;
|
|
2405
|
+
if (!session)
|
|
2406
|
+
return;
|
|
2407
|
+
this.#mailbox = new CodeMailbox2(dir, session.sessionId, session.sessionName ?? session.sessionId.slice(0, 8));
|
|
2408
|
+
this.#mailbox.open(cwd, (mail) => {
|
|
2409
|
+
const from = `${mail.fromName} (${mail.from.slice(0, 8)})`;
|
|
2410
|
+
this.#emit({ type: "notice", level: "info", message: `message from ${from}` });
|
|
2411
|
+
this.#prompt(`Message from another session \u2014 ${from}:
|
|
2412
|
+
${mail.text}`).catch((error) => this.#emit({ type: "notice", level: "error", message: String(error) }));
|
|
2413
|
+
});
|
|
2414
|
+
}
|
|
2415
|
+
get sessionId() {
|
|
2416
|
+
return this.#session?.sessionId ?? "";
|
|
2417
|
+
}
|
|
2418
|
+
get isStreaming() {
|
|
2419
|
+
return this.#session?.isStreaming ?? false;
|
|
2420
|
+
}
|
|
2421
|
+
get tokensUsed() {
|
|
2422
|
+
return this.#session?.getSessionStats().tokens.total ?? 0;
|
|
2423
|
+
}
|
|
2424
|
+
get info() {
|
|
2425
|
+
const session = this.#require();
|
|
2426
|
+
const model = session.model;
|
|
2427
|
+
return {
|
|
2428
|
+
sessionId: session.sessionId,
|
|
2429
|
+
cwd: this.#profile.paths.root,
|
|
2430
|
+
profile: this.#profile.name,
|
|
2431
|
+
model: model ? { provider: model.provider, id: model.id, name: model.name } : undefined,
|
|
2432
|
+
tools: session.getActiveToolNames(),
|
|
2433
|
+
contextTokens: model?.contextWindow,
|
|
2434
|
+
effort: session.supportsThinking() ? session.thinkingLevel : undefined,
|
|
2435
|
+
name: session.sessionName,
|
|
2436
|
+
interaction: this.#profile.interaction
|
|
2437
|
+
};
|
|
2438
|
+
}
|
|
2439
|
+
announce() {
|
|
2440
|
+
this.#emit({ type: "session", info: this.info });
|
|
2441
|
+
this.#restore();
|
|
2442
|
+
const { question, approvals } = this.#suspended;
|
|
2443
|
+
if (question)
|
|
2444
|
+
this.#emit({ type: "question", question });
|
|
2445
|
+
for (const request of approvals)
|
|
2446
|
+
this.#emit({ type: "approval", request });
|
|
2447
|
+
}
|
|
2448
|
+
#restore() {
|
|
2449
|
+
const session = this.#session;
|
|
2450
|
+
if (!session)
|
|
2451
|
+
return;
|
|
2452
|
+
const turnId = `restored:${session.sessionId}`;
|
|
2453
|
+
for (const message of session.messages) {
|
|
2454
|
+
if (message.role !== "user" && message.role !== "assistant")
|
|
2455
|
+
continue;
|
|
2456
|
+
const text = CodeAgent2.#textOf(message.content);
|
|
2457
|
+
if (text)
|
|
2458
|
+
this.#emit({ type: "message", turnId, role: message.role, text });
|
|
2459
|
+
}
|
|
2460
|
+
this.#emitContextNow();
|
|
2461
|
+
}
|
|
2462
|
+
static #textOf(content) {
|
|
2463
|
+
if (typeof content === "string")
|
|
2464
|
+
return content.trim();
|
|
2465
|
+
if (!Array.isArray(content))
|
|
2466
|
+
return "";
|
|
2467
|
+
return content.map((part) => typeof part === "object" && part && ("text" in part) ? String(part.text) : "").join("").trim();
|
|
2468
|
+
}
|
|
2469
|
+
on(listener) {
|
|
2470
|
+
this.#listeners.add(listener);
|
|
2471
|
+
return () => this.#listeners.delete(listener);
|
|
2472
|
+
}
|
|
2473
|
+
static replayLimit = 2000;
|
|
2474
|
+
async state({ sinceSeq } = {}) {
|
|
2475
|
+
const base = { info: this.info, streaming: this.isStreaming, replayFrom: this.#replayFrom };
|
|
2476
|
+
if (sinceSeq === undefined)
|
|
2477
|
+
return base;
|
|
2478
|
+
return { ...base, frames: this.#replay.filter((event) => event.seq > sinceSeq) };
|
|
2479
|
+
}
|
|
2480
|
+
async prompt(message, images) {
|
|
2481
|
+
this.#suspended.clear();
|
|
2482
|
+
await this.#prompt(message, images);
|
|
2483
|
+
}
|
|
2484
|
+
async#prompt(message, images) {
|
|
2485
|
+
const session = this.#require();
|
|
2486
|
+
if (!session.sessionName && message.trim())
|
|
2487
|
+
this.setName(codeAgentSessionName2(message));
|
|
2488
|
+
const attachments = await CodeAgent2.#attachments(images);
|
|
2489
|
+
const options = attachments.length ? { images: attachments } : {};
|
|
2490
|
+
if (session.isStreaming)
|
|
2491
|
+
return await session.prompt(message, { ...options, streamingBehavior: "followUp" });
|
|
2492
|
+
await session.prompt(message, { ...options, source: "extension" });
|
|
2493
|
+
}
|
|
2494
|
+
async abort() {
|
|
2495
|
+
this.#mapper.noteOutcome("aborted");
|
|
2496
|
+
this.#asks.clear();
|
|
2497
|
+
await this.#require().abort();
|
|
2498
|
+
}
|
|
2499
|
+
async answer(questionId, answer) {
|
|
2500
|
+
const question = this.#asks.questionOf(questionId);
|
|
2501
|
+
const rendered = question ? codeAgentRenderAnswer(question, answer) : answer.text ?? "";
|
|
2502
|
+
if (this.#asks.answer(questionId, rendered)) {
|
|
2503
|
+
this.#emit({ type: "question_resolved", questionId, answer, rendered });
|
|
2504
|
+
return true;
|
|
2505
|
+
}
|
|
2506
|
+
if (this.#profile.interaction.question !== "suspend")
|
|
2507
|
+
return false;
|
|
2508
|
+
const suspended = this.#suspended.takeQuestion(questionId);
|
|
2509
|
+
if (!suspended)
|
|
2510
|
+
return false;
|
|
2511
|
+
const suspendedRendered = codeAgentRenderAnswer(suspended, answer);
|
|
2512
|
+
this.#emit({ type: "question_resolved", questionId, answer, rendered: suspendedRendered });
|
|
2513
|
+
await this.#prompt(`The user answered: ${suspendedRendered}
|
|
2514
|
+
Continue the task.`);
|
|
2515
|
+
return true;
|
|
2516
|
+
}
|
|
2517
|
+
async approve(approvalId, approved) {
|
|
2518
|
+
if (this.#asks.resolveApproval(approvalId, approved)) {
|
|
2519
|
+
this.#emit({ type: "approval_resolved", approvalId, approved });
|
|
2520
|
+
return true;
|
|
2521
|
+
}
|
|
2522
|
+
if (this.#profile.interaction.approval !== "suspend")
|
|
2523
|
+
return false;
|
|
2524
|
+
if (!this.#suspended.takeApproval(approvalId))
|
|
2525
|
+
return false;
|
|
2526
|
+
this.#emit({ type: "approval_resolved", approvalId, approved });
|
|
2527
|
+
if (approved)
|
|
2528
|
+
await this.#prompt("The user approved the pending action. Retry it and continue.");
|
|
2529
|
+
return true;
|
|
2530
|
+
}
|
|
2531
|
+
async compact(instructions) {
|
|
2532
|
+
await this.#require().compact(instructions);
|
|
2533
|
+
}
|
|
2534
|
+
setName(name) {
|
|
2535
|
+
const clean = name.trim();
|
|
2536
|
+
if (!clean)
|
|
2537
|
+
return;
|
|
2538
|
+
this.#require().setSessionName(clean);
|
|
2539
|
+
this.#mailbox?.rename(clean, this.#profile.paths.root);
|
|
2540
|
+
this.#emit({ type: "session", info: this.info });
|
|
2541
|
+
}
|
|
2542
|
+
fork(name) {
|
|
2543
|
+
if (!this.#sessionDir)
|
|
2544
|
+
throw new Error("This profile keeps no sessions on disk, so there is nothing to fork");
|
|
2545
|
+
return CodeSessionFork2.fork(this.#sessionDir, this.sessionId, name);
|
|
2546
|
+
}
|
|
2547
|
+
efforts() {
|
|
2548
|
+
const session = this.#session;
|
|
2549
|
+
if (!session?.supportsThinking())
|
|
2550
|
+
return [];
|
|
2551
|
+
return session.getAvailableThinkingLevels();
|
|
2552
|
+
}
|
|
2553
|
+
setEffort(effort) {
|
|
2554
|
+
const session = this.#require();
|
|
2555
|
+
if (!session.supportsThinking())
|
|
2556
|
+
throw new Error(`${session.model?.id ?? "this model"} does no reasoning`);
|
|
2557
|
+
session.setThinkingLevel(effort);
|
|
2558
|
+
this.#emit({ type: "session", info: this.info });
|
|
2559
|
+
}
|
|
2560
|
+
catalogue() {
|
|
2561
|
+
const runtime = this.#session?.modelRuntime;
|
|
2562
|
+
if (!runtime)
|
|
2563
|
+
return [];
|
|
2564
|
+
const current = this.#session?.model;
|
|
2565
|
+
return runtime.getProviders().map((provider) => ({
|
|
2566
|
+
id: provider.id,
|
|
2567
|
+
name: provider.name ?? provider.id,
|
|
2568
|
+
authorized: runtime.hasConfiguredAuth(provider.id),
|
|
2569
|
+
models: (runtime.getModels(provider.id) ?? []).map((model) => ({
|
|
2570
|
+
id: model.id,
|
|
2571
|
+
name: model.name,
|
|
2572
|
+
...model.contextWindow ? { contextWindow: model.contextWindow } : {},
|
|
2573
|
+
current: model.provider === current?.provider && model.id === current?.id
|
|
2574
|
+
}))
|
|
2575
|
+
})).sort((a, b) => a.id.localeCompare(b.id));
|
|
2576
|
+
}
|
|
2577
|
+
async setModel(ref) {
|
|
2578
|
+
const session = this.#require();
|
|
2579
|
+
const model = await akanCodeModel2(session.modelRuntime, ref);
|
|
2580
|
+
if (!model)
|
|
2581
|
+
throw new Error(`Unknown model: ${ref.provider}/${ref.id}`);
|
|
2582
|
+
await session.setModel(model);
|
|
2583
|
+
this.#emit({ type: "session", info: this.info });
|
|
2584
|
+
}
|
|
2585
|
+
async waitForIdle() {
|
|
2586
|
+
await this.#require().waitForIdle();
|
|
2587
|
+
}
|
|
2588
|
+
dispose() {
|
|
2589
|
+
this.#mailbox?.close();
|
|
2590
|
+
if (this.#disposed)
|
|
2591
|
+
return;
|
|
2592
|
+
this.#disposed = true;
|
|
2593
|
+
this.#asks.clear();
|
|
2594
|
+
this.#listeners.clear();
|
|
2595
|
+
this.#disposePlugins?.();
|
|
2596
|
+
this.#session?.dispose();
|
|
2597
|
+
}
|
|
2598
|
+
async#attach(session, mode) {
|
|
2599
|
+
this.#session = session;
|
|
2600
|
+
session.subscribe((event) => {
|
|
2601
|
+
for (const body of this.#mapper.map(event))
|
|
2602
|
+
this.#emit(body);
|
|
2603
|
+
if (event.type === "agent_end" || event.type === "compaction_end")
|
|
2604
|
+
this.#emitContextNow();
|
|
2605
|
+
});
|
|
2606
|
+
await session.bindExtensions({
|
|
2607
|
+
mode,
|
|
2608
|
+
uiContext: this.#ui().context(),
|
|
2609
|
+
abortHandler: () => void session.abort(),
|
|
2610
|
+
onError: (error) => this.#emit({ type: "error", message: String(error), fatal: false })
|
|
2611
|
+
});
|
|
2612
|
+
}
|
|
2613
|
+
static async#attachments(images) {
|
|
2614
|
+
if (!images?.length)
|
|
2615
|
+
return [];
|
|
2616
|
+
return await Promise.all(images.map(async (image) => {
|
|
2617
|
+
if ("data" in image)
|
|
2618
|
+
return { type: "image", data: image.data, mimeType: image.mime };
|
|
2619
|
+
const file = Bun.file(image.path);
|
|
2620
|
+
return {
|
|
2621
|
+
type: "image",
|
|
2622
|
+
data: Buffer.from(await file.arrayBuffer()).toString("base64"),
|
|
2623
|
+
mimeType: file.type || "image/png"
|
|
2624
|
+
};
|
|
2625
|
+
}));
|
|
2626
|
+
}
|
|
2627
|
+
#emitContextNow() {
|
|
2628
|
+
const usage = this.#session?.getContextUsage();
|
|
2629
|
+
if (usage?.tokens == null)
|
|
2630
|
+
return;
|
|
2631
|
+
if (usage.tokens === this.#lastContextTokens)
|
|
2632
|
+
return;
|
|
2633
|
+
this.#lastContextTokens = usage.tokens;
|
|
2634
|
+
this.#emit({ type: "context", used: usage.tokens, max: usage.contextWindow || undefined });
|
|
2635
|
+
}
|
|
2636
|
+
#require() {
|
|
2637
|
+
if (!this.#session)
|
|
2638
|
+
throw new Error("CodeAgent is not started");
|
|
2639
|
+
return this.#session;
|
|
2640
|
+
}
|
|
2641
|
+
#emit(body) {
|
|
2642
|
+
this.#seq += 1;
|
|
2643
|
+
const event = { ...body, seq: this.#seq };
|
|
2644
|
+
this.#remember(event);
|
|
2645
|
+
for (const listener of this.#listeners)
|
|
2646
|
+
listener(event);
|
|
2647
|
+
}
|
|
2648
|
+
#remember(event) {
|
|
2649
|
+
if (event.type === "idle") {
|
|
2650
|
+
this.#replay.length = 0;
|
|
2651
|
+
this.#replayFrom = event.seq;
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
this.#replay.push(event);
|
|
2655
|
+
while (this.#replay.length > CodeAgent2.replayLimit) {
|
|
2656
|
+
const dropped = this.#replay.shift();
|
|
2657
|
+
this.#replayFrom = dropped?.seq ?? this.#replayFrom;
|
|
2658
|
+
}
|
|
2659
|
+
}
|
|
2660
|
+
#gateExtension() {
|
|
2661
|
+
return {
|
|
2662
|
+
name: "akan-profile-gate",
|
|
2663
|
+
factory: (pi) => {
|
|
2664
|
+
pi.on("tool_call", async (event) => {
|
|
2665
|
+
const verdict = this.#gate.verdict(event.toolName, event.input);
|
|
2666
|
+
if (verdict.block)
|
|
2667
|
+
return this.#refuse(event.toolCallId, event.toolName, event.input, verdict.block);
|
|
2668
|
+
if (!verdict.approval)
|
|
2669
|
+
return;
|
|
2670
|
+
const approved = await this.#requestApproval(event.toolCallId, event.toolName, event.input, verdict.approval);
|
|
2671
|
+
if (approved)
|
|
2672
|
+
return;
|
|
2673
|
+
return this.#refuse(event.toolCallId, event.toolName, event.input, "The user declined this action.");
|
|
2674
|
+
});
|
|
2675
|
+
}
|
|
2676
|
+
};
|
|
2677
|
+
}
|
|
2678
|
+
#refuse(toolCallId, toolName, args, reason) {
|
|
2679
|
+
for (const body of this.#mapper.markBlocked(toolCallId, toolName, args, reason))
|
|
2680
|
+
this.#emit(body);
|
|
2681
|
+
return { block: true, reason };
|
|
2682
|
+
}
|
|
2683
|
+
async#requestApproval(toolCallId, name, args, summary) {
|
|
2684
|
+
if (!this.#profile.ui.canPrompt)
|
|
2685
|
+
return true;
|
|
2686
|
+
const approvalId = this.#asks.nextId("a");
|
|
2687
|
+
const request = {
|
|
2688
|
+
approvalId,
|
|
2689
|
+
toolCallId,
|
|
2690
|
+
name,
|
|
2691
|
+
summary: codeAgentClip5(summary, codeAgentLabelChars2),
|
|
2692
|
+
policy: this.#profile.approval
|
|
2693
|
+
};
|
|
2694
|
+
this.#emit({ type: "approval", request });
|
|
2695
|
+
if (this.#profile.interaction.approval === "suspend") {
|
|
2696
|
+
this.#suspended.addApproval(request);
|
|
2697
|
+
this.#mapper.noteOutcome("awaiting");
|
|
2698
|
+
return false;
|
|
2699
|
+
}
|
|
2700
|
+
return await this.#asks.openApproval(approvalId);
|
|
2701
|
+
}
|
|
2702
|
+
async ask(spec) {
|
|
2703
|
+
const question = { questionId: this.#asks.nextId("q"), ...spec };
|
|
2704
|
+
if (!this.#profile.ui.canPrompt) {
|
|
2705
|
+
this.#emit({ type: "question_skipped", question, reason: "no-host" });
|
|
2706
|
+
return;
|
|
2707
|
+
}
|
|
2708
|
+
this.#emit({ type: "question", question });
|
|
2709
|
+
if (this.#profile.interaction.question === "suspend") {
|
|
2710
|
+
this.#suspended.setQuestion(question);
|
|
2711
|
+
this.#mapper.noteOutcome("awaiting");
|
|
2712
|
+
return;
|
|
2713
|
+
}
|
|
2714
|
+
return await this.#asks.openQuestion(question) || undefined;
|
|
2715
|
+
}
|
|
2716
|
+
#ui() {
|
|
2717
|
+
return new CodeAgentUi2({
|
|
2718
|
+
ask: async (prompt, kind, choices) => await this.ask({
|
|
2719
|
+
prompt,
|
|
2720
|
+
kind,
|
|
2721
|
+
...choices ? { options: choices.map((label) => ({ key: label, label })) } : {},
|
|
2722
|
+
freeText: kind === "text"
|
|
2723
|
+
}),
|
|
2724
|
+
notify: (level, message) => this.#emit({ type: "notice", level, message })
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
export { AkanEditScope2, DevLogFeedback2, PreviewChrome2, PreviewView2, PreviewFeedback2, TurnFeedback2, AkanCli2, AkanVerifier2, VerifyFeedback2, AkanToolPack2, MailToolPack2, McpUnauthorized2, McpClient2, McpToolPack2, SessionToolPack2, WebToolPack2, AkanCodePlugins2, AkanEnvKeys2, akanSystemPrompt2, CodeSessionIndex2, AkanCodeServices2, CodeAgentAsks2, CodeAgentEventMapper2, CodeAgentGate2, CodeAgentUi2, CodeMailbox2, CodeSessionFork2, CodeAgent2 };
|