larkin 0.2.70
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/CONTRIBUTING.md +48 -0
- package/LICENSE +201 -0
- package/README.md +122 -0
- package/SECURITY.md +14 -0
- package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
- package/dist/agent/agent-cli-capabilities.cjs +43 -0
- package/dist/agent/agent-cli-capabilities.mjs +35 -0
- package/dist/agent/agent-state-store.cjs +961 -0
- package/dist/agent/agent-state-store.mjs +923 -0
- package/dist/agent/agent-transport.cjs +201 -0
- package/dist/agent/agent-transport.mjs +194 -0
- package/dist/agent/config-cli-contract.cjs +19 -0
- package/dist/agent/config-cli-contract.mjs +16 -0
- package/dist/agent/context-prompt.cjs +159 -0
- package/dist/agent/context-prompt.mjs +155 -0
- package/dist/agent/freshness-gate.mjs +24 -0
- package/dist/agent/history-projection.cjs +72 -0
- package/dist/agent/history-projection.mjs +65 -0
- package/dist/agent/host-reminder-orchestrator.cjs +289 -0
- package/dist/agent/host-reminder-orchestrator.mjs +252 -0
- package/dist/agent/inbox-projection.cjs +92 -0
- package/dist/agent/inbox-projection.mjs +85 -0
- package/dist/agent/interaction-state-machine.cjs +710 -0
- package/dist/agent/interaction-state-machine.mjs +705 -0
- package/dist/agent/reminder-routes.cjs +192 -0
- package/dist/agent/reminder-routes.mjs +156 -0
- package/dist/agent/reminder-store.cjs +220 -0
- package/dist/agent/reminder-store.mjs +205 -0
- package/dist/agent/transport-business-context.cjs +486 -0
- package/dist/agent/transport-business-context.mjs +446 -0
- package/dist/agent/transport-shell.cjs +45 -0
- package/dist/agent/transport-shell.mjs +39 -0
- package/dist/app/agent-cli.mjs +596 -0
- package/dist/app/agent-config.mjs +633 -0
- package/dist/app/agent-lark-cli-workspace.cjs +95 -0
- package/dist/app/agent-lark-cli-workspace.mjs +85 -0
- package/dist/app/agent-readiness.cjs +42 -0
- package/dist/app/agent-readiness.mjs +38 -0
- package/dist/app/binary-entry.mjs +86 -0
- package/dist/app/cli.mjs +176 -0
- package/dist/app/dashboard.mjs +205 -0
- package/dist/app/internal-command.cjs +57 -0
- package/dist/app/internal-command.mjs +48 -0
- package/dist/app/lark-channel-secret.mjs +22 -0
- package/dist/app/lark-cli.mjs +887 -0
- package/dist/app/lark.mjs +45 -0
- package/dist/app/local-control.mjs +728 -0
- package/dist/app/official-lark-cli.cjs +138 -0
- package/dist/app/official-lark-cli.mjs +128 -0
- package/dist/app/pi-auth-cli.mjs +67 -0
- package/dist/app/run.mjs +364 -0
- package/dist/app/runtime-agent-config.mjs +416 -0
- package/dist/app/runtime-model-directory.mjs +78 -0
- package/dist/app/runtime-process.mjs +113 -0
- package/dist/app/session-cli.mjs +83 -0
- package/dist/app/setup-dashboard.mjs +26 -0
- package/dist/app/setup.mjs +166 -0
- package/dist/app/telemetry.mjs +83 -0
- package/dist/dashboard/dashboard-assets.mjs +21 -0
- package/dist/dashboard/dashboard-avatar.mjs +128 -0
- package/dist/dashboard/dashboard-config-controller.mjs +525 -0
- package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
- package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
- package/dist/dashboard/dashboard-template.mjs +29 -0
- package/dist/dashboard/dashboard-view-model.mjs +556 -0
- package/dist/dashboard/dashboard-workspace.mjs +104 -0
- package/dist/dashboard/web/assets/dashboard.css +3 -0
- package/dist/dashboard/web/assets/dashboard.js +49 -0
- package/dist/feishu/channel-transport.cjs +151 -0
- package/dist/feishu/channel-transport.mjs +148 -0
- package/dist/feishu/document-comment.cjs +120 -0
- package/dist/feishu/document-comment.mjs +109 -0
- package/dist/feishu/host-business-state.cjs +477 -0
- package/dist/feishu/host-business-state.mjs +462 -0
- package/dist/feishu/host-channel-business.cjs +161 -0
- package/dist/feishu/host-channel-business.mjs +157 -0
- package/dist/feishu/host-processing-eye.cjs +185 -0
- package/dist/feishu/host-processing-eye.mjs +181 -0
- package/dist/feishu/host-shell.cjs +1425 -0
- package/dist/feishu/host-shell.mjs +1385 -0
- package/dist/feishu/im-freshness-adapter.mjs +89 -0
- package/dist/feishu/interaction-orchestrator.cjs +279 -0
- package/dist/feishu/interaction-orchestrator.mjs +242 -0
- package/dist/feishu/lark-passthrough.mjs +70 -0
- package/dist/feishu/message-policy.cjs +111 -0
- package/dist/feishu/message-policy.mjs +98 -0
- package/dist/feishu/outbound-transport.cjs +221 -0
- package/dist/feishu/outbound-transport.mjs +215 -0
- package/dist/platform/build-info.cjs +25 -0
- package/dist/platform/build-info.mjs +18 -0
- package/dist/platform/callback-capability.cjs +301 -0
- package/dist/platform/callback-capability.mjs +252 -0
- package/dist/platform/check-bun-version.cjs +12 -0
- package/dist/platform/config.cjs +751 -0
- package/dist/platform/config.mjs +694 -0
- package/dist/platform/process-inspect.cjs +89 -0
- package/dist/platform/process-state.cjs +237 -0
- package/dist/platform/process-state.mjs +223 -0
- package/dist/platform/release-artifacts.mjs +61 -0
- package/dist/platform/root-layout.cjs +111 -0
- package/dist/platform/root-layout.mjs +72 -0
- package/dist/platform/telemetry-config.cjs +64 -0
- package/dist/platform/telemetry-config.mjs +56 -0
- package/dist/platform/telemetry-spool.cjs +661 -0
- package/dist/platform/telemetry-spool.mjs +653 -0
- package/dist/platform/telemetry-tracing.cjs +602 -0
- package/dist/platform/telemetry-tracing.mjs +596 -0
- package/dist/platform/telemetry-uploader.cjs +150 -0
- package/dist/platform/telemetry-uploader.mjs +146 -0
- package/dist/platform/workspace-service.cjs +469 -0
- package/dist/platform/workspace-service.mjs +430 -0
- package/dist/runtime/builtin-pi-assets.mjs +33 -0
- package/dist/runtime/claude-model-catalog.cjs +102 -0
- package/dist/runtime/claude-model-catalog.mjs +99 -0
- package/dist/runtime/codex-model-catalog.cjs +108 -0
- package/dist/runtime/codex-model-catalog.mjs +105 -0
- package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
- package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
- package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
- package/dist/runtime/pi-model-catalog.cjs +116 -0
- package/dist/runtime/pi-model-catalog.mjs +75 -0
- package/dist/runtime/pi-official-auth.mjs +480 -0
- package/dist/runtime/pi-provider-config.cjs +252 -0
- package/dist/runtime/pi-provider-config.mjs +239 -0
- package/dist/runtime/pi-rpc-client.cjs +255 -0
- package/dist/runtime/pi-rpc-client.mjs +251 -0
- package/dist/runtime/pi-subagent-injection.mjs +129 -0
- package/dist/runtime/pi-subagents.bundle.js +8990 -0
- package/dist/runtime/runtime-adapters.mjs +1019 -0
- package/dist/runtime/runtime-contracts.cjs +2 -0
- package/dist/runtime/runtime-contracts.mjs +1 -0
- package/dist/runtime/runtime-host.cjs +930 -0
- package/dist/runtime/runtime-host.mjs +922 -0
- package/dist/runtime/runtime-model-catalog.cjs +43 -0
- package/dist/runtime/runtime-model-catalog.mjs +40 -0
- package/dist/runtime/runtime-readiness.cjs +177 -0
- package/dist/runtime/runtime-readiness.mjs +133 -0
- package/dist/setup/bot-register.mjs +655 -0
- package/dist/setup/grant-scopes.mjs +133 -0
- package/dist/setup/run-credential-preflight.mjs +89 -0
- package/dist/setup/setup-agent-choice.mjs +172 -0
- package/dist/setup/setup-bind.mjs +337 -0
- package/dist/setup/setup-binding.mjs +52 -0
- package/package.json +112 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import processInspect from "./process-inspect.cjs";
|
|
5
|
+
const START = "<!-- larkin:platform-rules:start -->";
|
|
6
|
+
const END = "<!-- larkin:platform-rules:end -->";
|
|
7
|
+
const START_BYTES = Buffer.from(START, "ascii");
|
|
8
|
+
const END_BYTES = Buffer.from(END, "ascii");
|
|
9
|
+
const PROMPT_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
10
|
+
const AGENT_ID = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
11
|
+
const NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
12
|
+
const LOCK_FILE = "workspace-reconcile.lock.json";
|
|
13
|
+
const LOCK_RETRY_ATTEMPTS = 100;
|
|
14
|
+
const LOCK_RETRY_MS = 20;
|
|
15
|
+
const MALFORMED_LOCK_GRACE_MS = 1_000;
|
|
16
|
+
function isMissing(error) {
|
|
17
|
+
return !!error && typeof error === "object" && "code" in error && error.code === "ENOENT";
|
|
18
|
+
}
|
|
19
|
+
function identity(stat) {
|
|
20
|
+
return { dev: stat.dev, ino: stat.ino };
|
|
21
|
+
}
|
|
22
|
+
function sameIdentity(left, right) {
|
|
23
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
24
|
+
}
|
|
25
|
+
function inside(root, candidate) {
|
|
26
|
+
const relative = path.relative(root, candidate);
|
|
27
|
+
return relative !== "" && !relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative);
|
|
28
|
+
}
|
|
29
|
+
function occurrences(content, marker) {
|
|
30
|
+
const found = [];
|
|
31
|
+
for (let at = content.indexOf(marker); at >= 0; at = content.indexOf(marker, at + marker.length))
|
|
32
|
+
found.push(at);
|
|
33
|
+
return found;
|
|
34
|
+
}
|
|
35
|
+
function promptMigration(content, file) {
|
|
36
|
+
const starts = occurrences(content, START_BYTES);
|
|
37
|
+
const ends = occurrences(content, END_BYTES);
|
|
38
|
+
if (starts.length === 0 && ends.length === 0)
|
|
39
|
+
return { next: content, spans: [] };
|
|
40
|
+
if (starts.length !== 1 || ends.length !== 1 || starts[0] > ends[0]) {
|
|
41
|
+
throw new Error(`managed prompt markers are malformed or duplicate: ${file}`);
|
|
42
|
+
}
|
|
43
|
+
const start = starts[0];
|
|
44
|
+
const length = ends[0] + END_BYTES.length - start;
|
|
45
|
+
const next = Buffer.from(content);
|
|
46
|
+
next.fill(0x20, start, start + length);
|
|
47
|
+
return { next, spans: [{ start, length }] };
|
|
48
|
+
}
|
|
49
|
+
function readStableFd(fd, label) {
|
|
50
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
51
|
+
const before = fs.fstatSync(fd);
|
|
52
|
+
if (!before.isFile())
|
|
53
|
+
throw new Error(`prompt is no longer a regular file: ${label}`);
|
|
54
|
+
const size = before.size;
|
|
55
|
+
const content = Buffer.alloc(size);
|
|
56
|
+
let offset = 0;
|
|
57
|
+
while (offset < size) {
|
|
58
|
+
const read = fs.readSync(fd, content, offset, size - offset, offset);
|
|
59
|
+
if (read === 0)
|
|
60
|
+
break;
|
|
61
|
+
offset += read;
|
|
62
|
+
}
|
|
63
|
+
const after = fs.fstatSync(fd);
|
|
64
|
+
if (offset === size && after.size === size && after.mtimeMs === before.mtimeMs && after.ctimeMs === before.ctimeMs) {
|
|
65
|
+
return content;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`owner prompt remained under sustained concurrent writes: ${label}`);
|
|
69
|
+
}
|
|
70
|
+
function promptPlan(file) {
|
|
71
|
+
let stat;
|
|
72
|
+
try {
|
|
73
|
+
stat = fs.lstatSync(file);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (isMissing(error))
|
|
77
|
+
return null;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
if (stat.isSymbolicLink())
|
|
81
|
+
throw new Error(`unsafe prompt symlink: ${file}`);
|
|
82
|
+
if (!stat.isFile())
|
|
83
|
+
throw new Error(`prompt path is not a regular file: ${file}`);
|
|
84
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | NOFOLLOW);
|
|
85
|
+
try {
|
|
86
|
+
const opened = fs.fstatSync(fd);
|
|
87
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), identity(stat))) {
|
|
88
|
+
throw new Error(`prompt changed while opening: ${file}`);
|
|
89
|
+
}
|
|
90
|
+
const current = readStableFd(fd, file);
|
|
91
|
+
const migration = promptMigration(current, file);
|
|
92
|
+
return { file, current, ...migration, mode: opened.mode & 0o777, identity: identity(opened), fd };
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
fs.closeSync(fd);
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function assertPromptUnchanged(plan) {
|
|
100
|
+
let stat;
|
|
101
|
+
try {
|
|
102
|
+
stat = fs.lstatSync(plan.file);
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
throw new Error(`owner prompt changed concurrently: ${plan.file}`);
|
|
106
|
+
}
|
|
107
|
+
if (stat.isSymbolicLink() || !stat.isFile() ||
|
|
108
|
+
!sameIdentity(identity(stat), plan.identity) || (stat.mode & 0o777) !== plan.mode) {
|
|
109
|
+
throw new Error(`owner prompt inode or mode changed concurrently: ${plan.file}`);
|
|
110
|
+
}
|
|
111
|
+
const opened = fs.fstatSync(plan.fd);
|
|
112
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), plan.identity) || (opened.mode & 0o777) !== plan.mode) {
|
|
113
|
+
throw new Error(`owner prompt descriptor changed concurrently: ${plan.file}`);
|
|
114
|
+
}
|
|
115
|
+
const current = readStableFd(plan.fd, plan.file);
|
|
116
|
+
if (!current.equals(plan.current))
|
|
117
|
+
throw new Error(`owner prompt content changed concurrently: ${plan.file}`);
|
|
118
|
+
}
|
|
119
|
+
function directoryIdentity(file, label) {
|
|
120
|
+
const stat = fs.lstatSync(file);
|
|
121
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
122
|
+
throw new Error(`${label} is no longer a stable directory: ${file}`);
|
|
123
|
+
return identity(stat);
|
|
124
|
+
}
|
|
125
|
+
function assertDirectoryIdentity(file, expected, label) {
|
|
126
|
+
let current;
|
|
127
|
+
try {
|
|
128
|
+
current = directoryIdentity(file, label);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw new Error(`${label} changed during workspace reconciliation: ${file}`);
|
|
132
|
+
}
|
|
133
|
+
if (!sameIdentity(current, expected))
|
|
134
|
+
throw new Error(`${label} inode changed during workspace reconciliation: ${file}`);
|
|
135
|
+
}
|
|
136
|
+
function closePromptPlan(plan) {
|
|
137
|
+
try {
|
|
138
|
+
fs.closeSync(plan.fd);
|
|
139
|
+
}
|
|
140
|
+
catch { /* already closed during cleanup */ }
|
|
141
|
+
}
|
|
142
|
+
function writeManagedSpans(plan, beforeWrite) {
|
|
143
|
+
assertPromptUnchanged(plan);
|
|
144
|
+
const fd = fs.openSync(plan.file, fs.constants.O_RDWR | NOFOLLOW);
|
|
145
|
+
try {
|
|
146
|
+
const opened = fs.fstatSync(fd);
|
|
147
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), plan.identity) || (opened.mode & 0o777) !== plan.mode) {
|
|
148
|
+
throw new Error(`owner prompt changed while opening for migration: ${plan.file}`);
|
|
149
|
+
}
|
|
150
|
+
beforeWrite?.(plan.file);
|
|
151
|
+
assertPromptUnchanged(plan);
|
|
152
|
+
const current = readStableFd(fd, plan.file);
|
|
153
|
+
if (!current.equals(plan.current))
|
|
154
|
+
throw new Error(`owner prompt content changed concurrently: ${plan.file}`);
|
|
155
|
+
for (const span of plan.spans) {
|
|
156
|
+
const blank = Buffer.alloc(span.length, 0x20);
|
|
157
|
+
let offset = 0;
|
|
158
|
+
while (offset < blank.length) {
|
|
159
|
+
const written = fs.writeSync(fd, blank, offset, blank.length - offset, span.start + offset);
|
|
160
|
+
if (written === 0)
|
|
161
|
+
throw new Error(`managed prompt migration made no write progress: ${plan.file}`);
|
|
162
|
+
offset += written;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
fs.fsyncSync(fd);
|
|
166
|
+
const after = fs.fstatSync(fd);
|
|
167
|
+
const canonical = fs.lstatSync(plan.file);
|
|
168
|
+
if (!after.isFile() || !sameIdentity(identity(after), plan.identity) || (after.mode & 0o777) !== plan.mode ||
|
|
169
|
+
after.size !== plan.current.length || canonical.isSymbolicLink() || !canonical.isFile() ||
|
|
170
|
+
!sameIdentity(identity(canonical), plan.identity) || canonical.size !== plan.current.length) {
|
|
171
|
+
throw new Error(`owner prompt inode, mode, or size changed during migration: ${plan.file}`);
|
|
172
|
+
}
|
|
173
|
+
const installed = readStableFd(fd, plan.file);
|
|
174
|
+
if (!installed.equals(plan.next))
|
|
175
|
+
throw new Error(`managed prompt migration readback mismatch: ${plan.file}`);
|
|
176
|
+
}
|
|
177
|
+
finally {
|
|
178
|
+
fs.closeSync(fd);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
function sleepSync(ms) {
|
|
182
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
183
|
+
}
|
|
184
|
+
function lockRecord() {
|
|
185
|
+
const inspected = processInspect.inspectProcess(process.pid);
|
|
186
|
+
if (!inspected?.ok || !inspected.startToken) {
|
|
187
|
+
throw new Error(`workspace reconciliation cannot establish process ownership (${inspected?.reason || "inspection failed"})`);
|
|
188
|
+
}
|
|
189
|
+
return {
|
|
190
|
+
pid: process.pid,
|
|
191
|
+
processStartToken: inspected.startToken,
|
|
192
|
+
nonce: crypto.randomUUID(),
|
|
193
|
+
startedAt: new Date().toISOString(),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function isLockRecord(value) {
|
|
197
|
+
if (!value || typeof value !== "object")
|
|
198
|
+
return false;
|
|
199
|
+
const record = value;
|
|
200
|
+
return Number.isInteger(record.pid) && Number(record.pid) > 0 &&
|
|
201
|
+
typeof record.processStartToken === "string" && record.processStartToken.length > 0 &&
|
|
202
|
+
typeof record.nonce === "string" && record.nonce.length > 0 &&
|
|
203
|
+
typeof record.startedAt === "string";
|
|
204
|
+
}
|
|
205
|
+
function createLockFile(file, record) {
|
|
206
|
+
let fd;
|
|
207
|
+
try {
|
|
208
|
+
fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | NOFOLLOW, 0o600);
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
if (error && typeof error === "object" && "code" in error && error.code === "EEXIST")
|
|
212
|
+
return null;
|
|
213
|
+
throw error;
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
fs.fchmodSync(fd, 0o600);
|
|
217
|
+
fs.writeFileSync(fd, JSON.stringify(record) + "\n");
|
|
218
|
+
fs.fsyncSync(fd);
|
|
219
|
+
return identity(fs.fstatSync(fd));
|
|
220
|
+
}
|
|
221
|
+
finally {
|
|
222
|
+
fs.closeSync(fd);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
function readLockSnapshot(file) {
|
|
226
|
+
let stat;
|
|
227
|
+
try {
|
|
228
|
+
stat = fs.lstatSync(file);
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
if (isMissing(error))
|
|
232
|
+
return null;
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
if (stat.isSymbolicLink() || !stat.isFile())
|
|
236
|
+
throw new Error("workspace reconciliation lock path is unsafe");
|
|
237
|
+
let fd;
|
|
238
|
+
try {
|
|
239
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | NOFOLLOW);
|
|
240
|
+
}
|
|
241
|
+
catch (error) {
|
|
242
|
+
if (isMissing(error))
|
|
243
|
+
return null;
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
try {
|
|
247
|
+
const opened = fs.fstatSync(fd);
|
|
248
|
+
if (!opened.isFile() || !sameIdentity(identity(opened), identity(stat)))
|
|
249
|
+
return null;
|
|
250
|
+
let parsed = null;
|
|
251
|
+
try {
|
|
252
|
+
parsed = JSON.parse(fs.readFileSync(fd, "utf8"));
|
|
253
|
+
}
|
|
254
|
+
catch { /* malformed/in-flight record */ }
|
|
255
|
+
return {
|
|
256
|
+
record: isLockRecord(parsed) ? parsed : null,
|
|
257
|
+
identity: identity(opened),
|
|
258
|
+
ageMs: Math.max(0, Date.now() - opened.mtimeMs),
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
finally {
|
|
262
|
+
fs.closeSync(fd);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function lockState(snapshot) {
|
|
266
|
+
if (!snapshot.record)
|
|
267
|
+
return snapshot.ageMs >= MALFORMED_LOCK_GRACE_MS ? "stale" : "unknown";
|
|
268
|
+
if (!processInspect.pidAlive(snapshot.record.pid))
|
|
269
|
+
return "stale";
|
|
270
|
+
const inspected = processInspect.inspectProcess(snapshot.record.pid);
|
|
271
|
+
if (!inspected?.ok || !inspected.startToken) {
|
|
272
|
+
return processInspect.pidAlive(snapshot.record.pid) ? "unknown" : "stale";
|
|
273
|
+
}
|
|
274
|
+
return inspected.startToken === snapshot.record.processStartToken ? "live" : "stale";
|
|
275
|
+
}
|
|
276
|
+
function unlinkIfIdentity(file, expected) {
|
|
277
|
+
let stat;
|
|
278
|
+
try {
|
|
279
|
+
stat = fs.lstatSync(file);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
if (isMissing(error))
|
|
283
|
+
return false;
|
|
284
|
+
throw error;
|
|
285
|
+
}
|
|
286
|
+
if (!stat.isFile() || !sameIdentity(identity(stat), expected))
|
|
287
|
+
return false;
|
|
288
|
+
fs.unlinkSync(file);
|
|
289
|
+
return true;
|
|
290
|
+
}
|
|
291
|
+
function tryAcquireReclaimGuard(file, record) {
|
|
292
|
+
let guardIdentity = createLockFile(file, record);
|
|
293
|
+
if (!guardIdentity) {
|
|
294
|
+
const existing = readLockSnapshot(file);
|
|
295
|
+
if (!existing || lockState(existing) !== "stale" || !unlinkIfIdentity(file, existing.identity))
|
|
296
|
+
return null;
|
|
297
|
+
guardIdentity = createLockFile(file, record);
|
|
298
|
+
if (!guardIdentity)
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
const ownedIdentity = guardIdentity;
|
|
302
|
+
return () => {
|
|
303
|
+
try {
|
|
304
|
+
const current = readLockSnapshot(file);
|
|
305
|
+
if (current?.record?.pid === record.pid && current.record.nonce === record.nonce &&
|
|
306
|
+
sameIdentity(current.identity, ownedIdentity))
|
|
307
|
+
unlinkIfIdentity(file, ownedIdentity);
|
|
308
|
+
}
|
|
309
|
+
catch { /* another owner or already removed */ }
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
function acquireWorkspaceLock(lockDirReal) {
|
|
313
|
+
const file = path.join(lockDirReal, LOCK_FILE);
|
|
314
|
+
const reclaimFile = path.join(lockDirReal, `${LOCK_FILE}.reclaim`);
|
|
315
|
+
const record = lockRecord();
|
|
316
|
+
let lastOwnerPid = null;
|
|
317
|
+
for (let attempt = 0; attempt < LOCK_RETRY_ATTEMPTS; attempt += 1) {
|
|
318
|
+
const ownedIdentity = createLockFile(file, record);
|
|
319
|
+
if (ownedIdentity) {
|
|
320
|
+
return () => {
|
|
321
|
+
try {
|
|
322
|
+
const current = readLockSnapshot(file);
|
|
323
|
+
if (current?.record?.pid === record.pid && current.record.nonce === record.nonce &&
|
|
324
|
+
sameIdentity(current.identity, ownedIdentity))
|
|
325
|
+
unlinkIfIdentity(file, ownedIdentity);
|
|
326
|
+
}
|
|
327
|
+
catch { /* another owner or already removed */ }
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const observed = readLockSnapshot(file);
|
|
331
|
+
if (!observed)
|
|
332
|
+
continue;
|
|
333
|
+
lastOwnerPid = observed.record?.pid || null;
|
|
334
|
+
if (lockState(observed) === "stale") {
|
|
335
|
+
const releaseReclaim = tryAcquireReclaimGuard(reclaimFile, record);
|
|
336
|
+
if (releaseReclaim) {
|
|
337
|
+
try {
|
|
338
|
+
const latest = readLockSnapshot(file);
|
|
339
|
+
if (latest && lockState(latest) === "stale")
|
|
340
|
+
unlinkIfIdentity(file, latest.identity);
|
|
341
|
+
}
|
|
342
|
+
finally {
|
|
343
|
+
releaseReclaim();
|
|
344
|
+
}
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
sleepSync(LOCK_RETRY_MS);
|
|
349
|
+
}
|
|
350
|
+
throw new Error(`workspace reconciliation remained busy after bounded retry${lastOwnerPid ? ` (pid=${lastOwnerPid})` : ""}`);
|
|
351
|
+
}
|
|
352
|
+
export function reconcileAgentWorkspace(options) {
|
|
353
|
+
const { workspaceDir, trustedWorkspaceRoot, lockDir, agentId, testHooks } = options || {};
|
|
354
|
+
if (!AGENT_ID.test(agentId || "") || agentId === "." || agentId === ".." || /[\\/]/.test(agentId || "")) {
|
|
355
|
+
throw new Error(`invalid agent id: ${JSON.stringify(agentId)}`);
|
|
356
|
+
}
|
|
357
|
+
if (!workspaceDir || !trustedWorkspaceRoot || !lockDir) {
|
|
358
|
+
throw new Error("workspaceDir, trustedWorkspaceRoot, and lockDir are required");
|
|
359
|
+
}
|
|
360
|
+
const rootPath = path.resolve(trustedWorkspaceRoot);
|
|
361
|
+
const workspacePath = path.resolve(workspaceDir);
|
|
362
|
+
if (!inside(rootPath, workspacePath))
|
|
363
|
+
throw new Error(`workspace path escapes trusted workspace root: ${workspacePath}`);
|
|
364
|
+
fs.mkdirSync(rootPath, { recursive: true, mode: 0o700 });
|
|
365
|
+
if (!fs.statSync(rootPath).isDirectory())
|
|
366
|
+
throw new Error(`trusted workspace root is not a directory: ${rootPath}`);
|
|
367
|
+
let workspaceStat;
|
|
368
|
+
try {
|
|
369
|
+
workspaceStat = fs.lstatSync(workspacePath);
|
|
370
|
+
}
|
|
371
|
+
catch (error) {
|
|
372
|
+
if (!isMissing(error))
|
|
373
|
+
throw error;
|
|
374
|
+
fs.mkdirSync(workspacePath, { mode: 0o700 });
|
|
375
|
+
workspaceStat = fs.lstatSync(workspacePath);
|
|
376
|
+
}
|
|
377
|
+
if (workspaceStat.isSymbolicLink())
|
|
378
|
+
throw new Error(`workspace directory symlink is unsafe: ${workspacePath}`);
|
|
379
|
+
if (!fs.statSync(workspacePath).isDirectory())
|
|
380
|
+
throw new Error(`workspace is not a directory: ${workspacePath}`);
|
|
381
|
+
const rootReal = fs.realpathSync(rootPath);
|
|
382
|
+
const workspaceReal = fs.realpathSync(workspacePath);
|
|
383
|
+
if (!inside(rootReal, workspaceReal))
|
|
384
|
+
throw new Error(`workspace realpath is outside trusted workspace root: ${workspaceReal}`);
|
|
385
|
+
const lockDirPath = path.resolve(lockDir);
|
|
386
|
+
fs.mkdirSync(lockDirPath, { recursive: true, mode: 0o700 });
|
|
387
|
+
const lockDirReal = fs.realpathSync(lockDirPath);
|
|
388
|
+
if (lockDirReal === workspaceReal || inside(workspaceReal, lockDirReal)) {
|
|
389
|
+
throw new Error("workspace reconciliation lockDir must be outside the user workspace");
|
|
390
|
+
}
|
|
391
|
+
// From this point on, never traverse the mutable canonical bridge again. Capture stable directory identities,
|
|
392
|
+
// lock through the Agent's internal state directory, and re-check all directories before preflight/write.
|
|
393
|
+
const rootIdentity = directoryIdentity(rootReal, "trusted workspace root");
|
|
394
|
+
const workspaceIdentity = directoryIdentity(workspaceReal, "workspace");
|
|
395
|
+
const lockDirIdentity = directoryIdentity(lockDirReal, "workspace lock directory");
|
|
396
|
+
const releaseLock = acquireWorkspaceLock(lockDirReal);
|
|
397
|
+
const openPlans = [];
|
|
398
|
+
try {
|
|
399
|
+
assertDirectoryIdentity(rootReal, rootIdentity, "trusted workspace root");
|
|
400
|
+
assertDirectoryIdentity(workspaceReal, workspaceIdentity, "workspace");
|
|
401
|
+
assertDirectoryIdentity(lockDirReal, lockDirIdentity, "workspace lock directory");
|
|
402
|
+
const plans = PROMPT_FILES.map((name) => promptPlan(path.join(workspaceReal, name)))
|
|
403
|
+
.filter((plan) => plan !== null);
|
|
404
|
+
openPlans.push(...plans);
|
|
405
|
+
const changed = plans.filter((plan) => !plan.next.equals(plan.current));
|
|
406
|
+
assertDirectoryIdentity(rootReal, rootIdentity, "trusted workspace root");
|
|
407
|
+
assertDirectoryIdentity(workspaceReal, workspaceIdentity, "workspace");
|
|
408
|
+
assertDirectoryIdentity(lockDirReal, lockDirIdentity, "workspace lock directory");
|
|
409
|
+
for (const plan of plans)
|
|
410
|
+
assertPromptUnchanged(plan);
|
|
411
|
+
const writtenPlans = new Set();
|
|
412
|
+
for (const plan of changed) {
|
|
413
|
+
assertDirectoryIdentity(rootReal, rootIdentity, "trusted workspace root");
|
|
414
|
+
assertDirectoryIdentity(workspaceReal, workspaceIdentity, "workspace");
|
|
415
|
+
assertDirectoryIdentity(lockDirReal, lockDirIdentity, "workspace lock directory");
|
|
416
|
+
for (const pending of plans) {
|
|
417
|
+
if (!writtenPlans.has(pending))
|
|
418
|
+
assertPromptUnchanged(pending);
|
|
419
|
+
}
|
|
420
|
+
writeManagedSpans(plan, testHooks?.beforeWrite);
|
|
421
|
+
writtenPlans.add(plan);
|
|
422
|
+
}
|
|
423
|
+
return { workspaceDir: workspaceReal, changed: changed.map((plan) => path.basename(plan.file)) };
|
|
424
|
+
}
|
|
425
|
+
finally {
|
|
426
|
+
for (const plan of openPlans)
|
|
427
|
+
closePromptPlan(plan);
|
|
428
|
+
releaseLock();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
function privateDirectory(directory) {
|
|
4
|
+
fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
5
|
+
const stat = fs.lstatSync(directory);
|
|
6
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
7
|
+
throw new Error(`unsafe bundled Pi asset directory: ${directory}`);
|
|
8
|
+
fs.chmodSync(directory, 0o700);
|
|
9
|
+
}
|
|
10
|
+
function writeEmbeddedFile(file, contents) {
|
|
11
|
+
const temporary = `${file}.${process.pid}.${Math.random().toString(16).slice(2)}.tmp`;
|
|
12
|
+
fs.writeFileSync(temporary, contents, { mode: 0o600, flag: "wx" });
|
|
13
|
+
fs.renameSync(temporary, file);
|
|
14
|
+
fs.chmodSync(file, 0o600);
|
|
15
|
+
}
|
|
16
|
+
/** Materialize upstream runtime assets that Bun cannot serve through ordinary fs paths. */
|
|
17
|
+
export function prepareBuiltinPiPackageAssets() {
|
|
18
|
+
const assets = globalThis.__LARKIN_EMBEDDED_BUILTIN_PI_ASSETS__;
|
|
19
|
+
if (!assets)
|
|
20
|
+
return;
|
|
21
|
+
const agentDir = process.env.PI_CODING_AGENT_DIR;
|
|
22
|
+
if (!agentDir)
|
|
23
|
+
throw new Error("bundled Pi requires PI_CODING_AGENT_DIR");
|
|
24
|
+
privateDirectory(agentDir);
|
|
25
|
+
const packageDir = path.join(agentDir, ".larkin-official-pi-package");
|
|
26
|
+
const themeDir = path.join(packageDir, "theme");
|
|
27
|
+
privateDirectory(packageDir);
|
|
28
|
+
privateDirectory(themeDir);
|
|
29
|
+
writeEmbeddedFile(path.join(packageDir, "package.json"), assets.packageJson);
|
|
30
|
+
writeEmbeddedFile(path.join(themeDir, "dark.json"), assets.darkTheme);
|
|
31
|
+
writeEmbeddedFile(path.join(themeDir, "light.json"), assets.lightTheme);
|
|
32
|
+
process.env.PI_PACKAGE_DIR = packageDir;
|
|
33
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.discoverClaudeModelCatalog = discoverClaudeModelCatalog;
|
|
4
|
+
const node_child_process_1 = require("node:child_process");
|
|
5
|
+
const SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,127}$/;
|
|
6
|
+
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
7
|
+
function cleanText(value, fallback) {
|
|
8
|
+
if (typeof value !== "string")
|
|
9
|
+
return fallback;
|
|
10
|
+
return value.replace(/[\u0000-\u001f\u007f]+/g, "").replace(/\s+/g, " ").trim().slice(0, 120) || fallback;
|
|
11
|
+
}
|
|
12
|
+
async function runClaudeControl(call) {
|
|
13
|
+
return await new Promise((resolve, reject) => {
|
|
14
|
+
const child = (0, node_child_process_1.spawn)(call.command || "claude", call.args, { ...(call.cwd ? { cwd: call.cwd } : {}), env: call.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
15
|
+
let settled = false;
|
|
16
|
+
let stdout = "";
|
|
17
|
+
let bytes = 0;
|
|
18
|
+
const requestId = String(call.request.request_id);
|
|
19
|
+
const finish = (error, value) => {
|
|
20
|
+
if (settled)
|
|
21
|
+
return;
|
|
22
|
+
settled = true;
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
25
|
+
child.kill("SIGTERM");
|
|
26
|
+
if (error)
|
|
27
|
+
reject(error);
|
|
28
|
+
else
|
|
29
|
+
resolve(value);
|
|
30
|
+
};
|
|
31
|
+
const timer = setTimeout(() => finish(new Error("Claude model catalog timed out")), call.timeout);
|
|
32
|
+
timer.unref?.();
|
|
33
|
+
child.on("error", () => finish(new Error("Claude model catalog command failed")));
|
|
34
|
+
child.on("exit", () => finish(new Error("Claude model catalog control channel exited before response")));
|
|
35
|
+
child.stderr.resume();
|
|
36
|
+
child.stdout.on("data", (chunk) => {
|
|
37
|
+
bytes += chunk.length;
|
|
38
|
+
if (bytes > call.maxBuffer)
|
|
39
|
+
return finish(new Error("Claude model catalog response exceeded limit"));
|
|
40
|
+
stdout += chunk.toString("utf8");
|
|
41
|
+
for (;;) {
|
|
42
|
+
const newline = stdout.indexOf("\n");
|
|
43
|
+
if (newline < 0)
|
|
44
|
+
break;
|
|
45
|
+
const line = stdout.slice(0, newline);
|
|
46
|
+
stdout = stdout.slice(newline + 1);
|
|
47
|
+
try {
|
|
48
|
+
const message = JSON.parse(line);
|
|
49
|
+
const response = message.response;
|
|
50
|
+
if (message.type === "control_response" && response?.request_id === requestId) {
|
|
51
|
+
if (response.subtype !== "success")
|
|
52
|
+
return finish(new Error("Claude model catalog request failed"));
|
|
53
|
+
return finish(undefined, response.response);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch { /* control channel is newline-delimited JSON; ignore unrelated messages */ }
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
child.stdin.write(`${JSON.stringify(call.request)}\n`);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
function efforts(item) {
|
|
63
|
+
if (item.supportsEffort !== true || !Array.isArray(item.supportedEffortLevels))
|
|
64
|
+
return [];
|
|
65
|
+
return [...new Set(item.supportedEffortLevels.filter((level) => typeof level === "string" && EFFORT_LEVELS.has(level)))];
|
|
66
|
+
}
|
|
67
|
+
function parseCatalog(value) {
|
|
68
|
+
const rows = value?.models;
|
|
69
|
+
if (!Array.isArray(rows) || rows.length === 0 || rows.length > 128)
|
|
70
|
+
throw new Error("Claude model catalog has an invalid model list");
|
|
71
|
+
const defaultRow = rows.find((row) => row?.value === "default");
|
|
72
|
+
const effectiveModel = typeof defaultRow?.resolvedModel === "string" && SAFE_MODEL_ID.test(defaultRow.resolvedModel) ? defaultRow.resolvedModel : "";
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
const models = rows.flatMap((row) => {
|
|
75
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
76
|
+
return [];
|
|
77
|
+
const item = row;
|
|
78
|
+
const id = typeof item.value === "string" ? item.value : "";
|
|
79
|
+
if (id === "default" || !SAFE_MODEL_ID.test(id) || seen.has(id))
|
|
80
|
+
return [];
|
|
81
|
+
seen.add(id);
|
|
82
|
+
return [{ id, label: cleanText(item.displayName, id), supportedReasoningEfforts: efforts(item), verified: "claude-control-visible" }];
|
|
83
|
+
});
|
|
84
|
+
if (!effectiveModel || !models.length)
|
|
85
|
+
throw new Error("Claude model catalog has no visible local default");
|
|
86
|
+
return { models, effectiveModel, defaultSupportedReasoningEfforts: efforts(defaultRow || {}) };
|
|
87
|
+
}
|
|
88
|
+
async function discoverClaudeModelCatalog(options = {}) {
|
|
89
|
+
const execute = options.runClaudeControl ?? runClaudeControl;
|
|
90
|
+
const request = { type: "control_request", request_id: "larkin-model-list", request: { subtype: "list_models" } };
|
|
91
|
+
try {
|
|
92
|
+
return parseCatalog(await execute({ args: ["--safe-mode", "--print", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
|
|
93
|
+
...(options.command ? { command: options.command } : {}),
|
|
94
|
+
...(options.cwd ? { cwd: options.cwd } : {}), env: { ...process.env, ...options.env, NO_COLOR: "1" },
|
|
95
|
+
maxBuffer: 1024 * 1024, request, timeout: 15_000 }));
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error instanceof Error && /Claude model catalog/.test(error.message))
|
|
99
|
+
throw error;
|
|
100
|
+
throw new Error("Claude model catalog unavailable");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
const SAFE_MODEL_ID = /^[A-Za-z0-9][A-Za-z0-9._\[\]-]{0,127}$/;
|
|
3
|
+
const EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh", "max"]);
|
|
4
|
+
function cleanText(value, fallback) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return fallback;
|
|
7
|
+
return value.replace(/[\u0000-\u001f\u007f]+/g, "").replace(/\s+/g, " ").trim().slice(0, 120) || fallback;
|
|
8
|
+
}
|
|
9
|
+
async function runClaudeControl(call) {
|
|
10
|
+
return await new Promise((resolve, reject) => {
|
|
11
|
+
const child = spawn(call.command || "claude", call.args, { ...(call.cwd ? { cwd: call.cwd } : {}), env: call.env, stdio: ["pipe", "pipe", "pipe"] });
|
|
12
|
+
let settled = false;
|
|
13
|
+
let stdout = "";
|
|
14
|
+
let bytes = 0;
|
|
15
|
+
const requestId = String(call.request.request_id);
|
|
16
|
+
const finish = (error, value) => {
|
|
17
|
+
if (settled)
|
|
18
|
+
return;
|
|
19
|
+
settled = true;
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
22
|
+
child.kill("SIGTERM");
|
|
23
|
+
if (error)
|
|
24
|
+
reject(error);
|
|
25
|
+
else
|
|
26
|
+
resolve(value);
|
|
27
|
+
};
|
|
28
|
+
const timer = setTimeout(() => finish(new Error("Claude model catalog timed out")), call.timeout);
|
|
29
|
+
timer.unref?.();
|
|
30
|
+
child.on("error", () => finish(new Error("Claude model catalog command failed")));
|
|
31
|
+
child.on("exit", () => finish(new Error("Claude model catalog control channel exited before response")));
|
|
32
|
+
child.stderr.resume();
|
|
33
|
+
child.stdout.on("data", (chunk) => {
|
|
34
|
+
bytes += chunk.length;
|
|
35
|
+
if (bytes > call.maxBuffer)
|
|
36
|
+
return finish(new Error("Claude model catalog response exceeded limit"));
|
|
37
|
+
stdout += chunk.toString("utf8");
|
|
38
|
+
for (;;) {
|
|
39
|
+
const newline = stdout.indexOf("\n");
|
|
40
|
+
if (newline < 0)
|
|
41
|
+
break;
|
|
42
|
+
const line = stdout.slice(0, newline);
|
|
43
|
+
stdout = stdout.slice(newline + 1);
|
|
44
|
+
try {
|
|
45
|
+
const message = JSON.parse(line);
|
|
46
|
+
const response = message.response;
|
|
47
|
+
if (message.type === "control_response" && response?.request_id === requestId) {
|
|
48
|
+
if (response.subtype !== "success")
|
|
49
|
+
return finish(new Error("Claude model catalog request failed"));
|
|
50
|
+
return finish(undefined, response.response);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch { /* control channel is newline-delimited JSON; ignore unrelated messages */ }
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
child.stdin.write(`${JSON.stringify(call.request)}\n`);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function efforts(item) {
|
|
60
|
+
if (item.supportsEffort !== true || !Array.isArray(item.supportedEffortLevels))
|
|
61
|
+
return [];
|
|
62
|
+
return [...new Set(item.supportedEffortLevels.filter((level) => typeof level === "string" && EFFORT_LEVELS.has(level)))];
|
|
63
|
+
}
|
|
64
|
+
function parseCatalog(value) {
|
|
65
|
+
const rows = value?.models;
|
|
66
|
+
if (!Array.isArray(rows) || rows.length === 0 || rows.length > 128)
|
|
67
|
+
throw new Error("Claude model catalog has an invalid model list");
|
|
68
|
+
const defaultRow = rows.find((row) => row?.value === "default");
|
|
69
|
+
const effectiveModel = typeof defaultRow?.resolvedModel === "string" && SAFE_MODEL_ID.test(defaultRow.resolvedModel) ? defaultRow.resolvedModel : "";
|
|
70
|
+
const seen = new Set();
|
|
71
|
+
const models = rows.flatMap((row) => {
|
|
72
|
+
if (!row || typeof row !== "object" || Array.isArray(row))
|
|
73
|
+
return [];
|
|
74
|
+
const item = row;
|
|
75
|
+
const id = typeof item.value === "string" ? item.value : "";
|
|
76
|
+
if (id === "default" || !SAFE_MODEL_ID.test(id) || seen.has(id))
|
|
77
|
+
return [];
|
|
78
|
+
seen.add(id);
|
|
79
|
+
return [{ id, label: cleanText(item.displayName, id), supportedReasoningEfforts: efforts(item), verified: "claude-control-visible" }];
|
|
80
|
+
});
|
|
81
|
+
if (!effectiveModel || !models.length)
|
|
82
|
+
throw new Error("Claude model catalog has no visible local default");
|
|
83
|
+
return { models, effectiveModel, defaultSupportedReasoningEfforts: efforts(defaultRow || {}) };
|
|
84
|
+
}
|
|
85
|
+
export async function discoverClaudeModelCatalog(options = {}) {
|
|
86
|
+
const execute = options.runClaudeControl ?? runClaudeControl;
|
|
87
|
+
const request = { type: "control_request", request_id: "larkin-model-list", request: { subtype: "list_models" } };
|
|
88
|
+
try {
|
|
89
|
+
return parseCatalog(await execute({ args: ["--safe-mode", "--print", "--input-format", "stream-json", "--output-format", "stream-json", "--verbose"],
|
|
90
|
+
...(options.command ? { command: options.command } : {}),
|
|
91
|
+
...(options.cwd ? { cwd: options.cwd } : {}), env: { ...process.env, ...options.env, NO_COLOR: "1" },
|
|
92
|
+
maxBuffer: 1024 * 1024, request, timeout: 15_000 }));
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error instanceof Error && /Claude model catalog/.test(error.message))
|
|
96
|
+
throw error;
|
|
97
|
+
throw new Error("Claude model catalog unavailable");
|
|
98
|
+
}
|
|
99
|
+
}
|