@linzumi/cli 0.0.20-beta → 0.0.22-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -62
- package/bin/linzumi.js +10 -18
- package/dist/assets/linzumi-logo.svg +1 -0
- package/dist/index.js +9135 -0
- package/package.json +9 -4
- package/src/agentBootstrap.ts +0 -872
- package/src/authCache.ts +0 -157
- package/src/authResolution.ts +0 -77
- package/src/boundedCache.ts +0 -57
- package/src/channelSession.ts +0 -4301
- package/src/channelSessionSupport.ts +0 -308
- package/src/codexAppServer.ts +0 -380
- package/src/codexOutput.ts +0 -846
- package/src/codexRuntimeOptions.ts +0 -80
- package/src/dependencyStatus.ts +0 -198
- package/src/forwardTunnel.ts +0 -859
- package/src/forwardTunnelProtocol.ts +0 -324
- package/src/index.ts +0 -1080
- package/src/json.ts +0 -49
- package/src/kandanQueue.ts +0 -113
- package/src/kandanTls.ts +0 -86
- package/src/localCapabilities.ts +0 -143
- package/src/localCodexMessageState.ts +0 -135
- package/src/localCodexTurnState.ts +0 -108
- package/src/localConfig.ts +0 -99
- package/src/localEditor.ts +0 -1061
- package/src/localEditorRuntime.ts +0 -717
- package/src/localForwarding.ts +0 -523
- package/src/oauth.ts +0 -425
- package/src/pendingKandanMessageQueue.ts +0 -109
- package/src/phoenix.ts +0 -359
- package/src/portForwardApproval.ts +0 -181
- package/src/portForwardWatcher.ts +0 -404
- package/src/protocol.ts +0 -321
- package/src/runner.ts +0 -943
- package/src/runnerConsoleReporter.ts +0 -142
- package/src/runnerLogger.ts +0 -50
- package/src/streamDeltaCoalescing.ts +0 -129
- package/src/streamDeltaQueue.ts +0 -102
package/src/agentBootstrap.ts
DELETED
|
@@ -1,872 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
- Date: 2026-05-02
|
|
3
|
-
Spec: plans/2026-05-02-agent-first-zero-to-codex-launch-plan.md
|
|
4
|
-
Relationship: Implements the CLI side of the agent-first bootstrap contract:
|
|
5
|
-
signup, claim, thread creation, progress, inbox, and done messages against
|
|
6
|
-
the external /agent/* machine API.
|
|
7
|
-
*/
|
|
8
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
10
|
-
import { homedir } from "node:os";
|
|
11
|
-
import { type JsonObject, type JsonValue, isJsonObject } from "./protocol";
|
|
12
|
-
import { arrayValue, objectValue, stringValue } from "./json";
|
|
13
|
-
|
|
14
|
-
const defaultApiUrl = "https://serve.linzumi.com";
|
|
15
|
-
|
|
16
|
-
type AgentCommand =
|
|
17
|
-
| { readonly kind: "help" }
|
|
18
|
-
| {
|
|
19
|
-
readonly kind: "signup";
|
|
20
|
-
readonly apiUrl: string;
|
|
21
|
-
readonly email: string;
|
|
22
|
-
readonly agentName: string;
|
|
23
|
-
}
|
|
24
|
-
| {
|
|
25
|
-
readonly kind: "claim";
|
|
26
|
-
readonly apiUrl: string;
|
|
27
|
-
readonly pendingId: string;
|
|
28
|
-
readonly code: string;
|
|
29
|
-
readonly tokenFile: string;
|
|
30
|
-
}
|
|
31
|
-
| {
|
|
32
|
-
readonly kind: "threadNew";
|
|
33
|
-
readonly apiUrl: string;
|
|
34
|
-
readonly title: string;
|
|
35
|
-
readonly message: string;
|
|
36
|
-
readonly tokenFile: string;
|
|
37
|
-
}
|
|
38
|
-
| {
|
|
39
|
-
readonly kind: "channelPost";
|
|
40
|
-
readonly apiUrl: string;
|
|
41
|
-
readonly channelId: string;
|
|
42
|
-
readonly body: string;
|
|
43
|
-
readonly tokenFile: string;
|
|
44
|
-
}
|
|
45
|
-
| {
|
|
46
|
-
readonly kind: "post";
|
|
47
|
-
readonly apiUrl: string;
|
|
48
|
-
readonly threadId: string;
|
|
49
|
-
readonly body: string;
|
|
50
|
-
readonly messageKind: "progress" | "question";
|
|
51
|
-
readonly tokenFile: string;
|
|
52
|
-
}
|
|
53
|
-
| {
|
|
54
|
-
readonly kind: "inbox";
|
|
55
|
-
readonly apiUrl: string;
|
|
56
|
-
readonly threadId: string;
|
|
57
|
-
readonly sinceLast: boolean;
|
|
58
|
-
readonly tokenFile: string;
|
|
59
|
-
}
|
|
60
|
-
| {
|
|
61
|
-
readonly kind: "done";
|
|
62
|
-
readonly apiUrl: string;
|
|
63
|
-
readonly threadId: string;
|
|
64
|
-
readonly message: string;
|
|
65
|
-
readonly tokenFile: string;
|
|
66
|
-
}
|
|
67
|
-
| {
|
|
68
|
-
readonly kind: "codexStart";
|
|
69
|
-
readonly apiUrl: string;
|
|
70
|
-
readonly threadId: string;
|
|
71
|
-
readonly runnerId: string;
|
|
72
|
-
readonly cwd: string;
|
|
73
|
-
readonly workDescription: string;
|
|
74
|
-
readonly model: string | undefined;
|
|
75
|
-
readonly reasoningEffort: string | undefined;
|
|
76
|
-
readonly approvalPolicy: string | undefined;
|
|
77
|
-
readonly sandbox: string | undefined;
|
|
78
|
-
readonly fast: boolean;
|
|
79
|
-
readonly tokenFile: string;
|
|
80
|
-
}
|
|
81
|
-
| {
|
|
82
|
-
readonly kind: "editorOpen";
|
|
83
|
-
readonly apiUrl: string;
|
|
84
|
-
readonly threadId: string;
|
|
85
|
-
readonly runnerId: string;
|
|
86
|
-
readonly cwd: string;
|
|
87
|
-
readonly tokenFile: string;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
export type AgentTokenFile = {
|
|
91
|
-
readonly apiUrl: string;
|
|
92
|
-
readonly agentToken: string;
|
|
93
|
-
readonly agentId: string;
|
|
94
|
-
readonly workspaceId: string;
|
|
95
|
-
readonly channelId: string | undefined;
|
|
96
|
-
readonly ownerUsername: string | undefined;
|
|
97
|
-
readonly channelUrl: string;
|
|
98
|
-
readonly loginUrl: string;
|
|
99
|
-
readonly supportChannelId: string;
|
|
100
|
-
readonly supportChannelUrl: string | undefined;
|
|
101
|
-
readonly savedAt: string;
|
|
102
|
-
readonly cursors: Record<string, string>;
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
type AgentCliDeps = {
|
|
106
|
-
readonly fetchImpl: typeof fetch;
|
|
107
|
-
readonly stdout: Pick<typeof process.stdout, "write">;
|
|
108
|
-
readonly readTextFile: (path: string) => string | undefined;
|
|
109
|
-
readonly writeTextFile: (path: string, content: string) => void;
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export async function runAgentCliCommand(
|
|
113
|
-
args: readonly string[],
|
|
114
|
-
deps: AgentCliDeps = {
|
|
115
|
-
fetchImpl: fetch,
|
|
116
|
-
stdout: process.stdout,
|
|
117
|
-
readTextFile: readOptionalTextFile,
|
|
118
|
-
writeTextFile,
|
|
119
|
-
},
|
|
120
|
-
): Promise<void> {
|
|
121
|
-
const command = parseAgentCommand(args);
|
|
122
|
-
|
|
123
|
-
switch (command.kind) {
|
|
124
|
-
case "help":
|
|
125
|
-
deps.stdout.write(agentHelpText());
|
|
126
|
-
return;
|
|
127
|
-
case "signup":
|
|
128
|
-
await runSignup(command, deps);
|
|
129
|
-
return;
|
|
130
|
-
case "claim":
|
|
131
|
-
await runClaim(command, deps);
|
|
132
|
-
return;
|
|
133
|
-
case "threadNew":
|
|
134
|
-
await runThreadNew(command, deps);
|
|
135
|
-
return;
|
|
136
|
-
case "channelPost":
|
|
137
|
-
await runChannelPost(command, deps);
|
|
138
|
-
return;
|
|
139
|
-
case "post":
|
|
140
|
-
await runPost(command, deps);
|
|
141
|
-
return;
|
|
142
|
-
case "inbox":
|
|
143
|
-
await runInbox(command, deps);
|
|
144
|
-
return;
|
|
145
|
-
case "done":
|
|
146
|
-
await runDone(command, deps);
|
|
147
|
-
return;
|
|
148
|
-
case "codexStart":
|
|
149
|
-
await runCodexStart(command, deps);
|
|
150
|
-
return;
|
|
151
|
-
case "editorOpen":
|
|
152
|
-
await runEditorOpen(command, deps);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function parseAgentCommand(args: readonly string[]): AgentCommand {
|
|
158
|
-
const [first, ...rest] = args;
|
|
159
|
-
|
|
160
|
-
switch (first) {
|
|
161
|
-
case undefined:
|
|
162
|
-
case "--help":
|
|
163
|
-
case "-h":
|
|
164
|
-
case "help":
|
|
165
|
-
return { kind: "help" };
|
|
166
|
-
case "signup": {
|
|
167
|
-
const parsed = parseAgentArgs(rest);
|
|
168
|
-
return {
|
|
169
|
-
kind: "signup",
|
|
170
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
171
|
-
email: requiredFlag(parsed.flags, "email"),
|
|
172
|
-
agentName: requiredFlag(parsed.flags, "agent-name"),
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
case "claim": {
|
|
176
|
-
const parsed = parseAgentArgs(rest);
|
|
177
|
-
return {
|
|
178
|
-
kind: "claim",
|
|
179
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
180
|
-
pendingId: requiredFlag(parsed.flags, "pending"),
|
|
181
|
-
code: requiredFlag(parsed.flags, "code"),
|
|
182
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
183
|
-
};
|
|
184
|
-
}
|
|
185
|
-
case "thread": {
|
|
186
|
-
const [subcommand, ...subcommandArgs] = rest;
|
|
187
|
-
|
|
188
|
-
if (subcommand !== "new") {
|
|
189
|
-
throw new Error("linzumi thread supports: new");
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const parsed = parseAgentArgs(subcommandArgs);
|
|
193
|
-
const [title, ...extra] = parsed.positionals;
|
|
194
|
-
|
|
195
|
-
if (title === undefined || title.trim() === "") {
|
|
196
|
-
throw new Error("missing thread title");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
if (extra.length > 0) {
|
|
200
|
-
throw new Error("linzumi thread new accepts one title");
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
return {
|
|
204
|
-
kind: "threadNew",
|
|
205
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
206
|
-
title,
|
|
207
|
-
message: requiredFlag(parsed.flags, "message"),
|
|
208
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
case "channel": {
|
|
212
|
-
const [subcommand, ...subcommandArgs] = rest;
|
|
213
|
-
|
|
214
|
-
if (subcommand !== "post") {
|
|
215
|
-
throw new Error("linzumi channel supports: post");
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const parsed = parseAgentArgs(subcommandArgs);
|
|
219
|
-
const [channelId, body, ...extra] = parsed.positionals;
|
|
220
|
-
|
|
221
|
-
if (channelId === undefined || channelId.trim() === "") {
|
|
222
|
-
throw new Error("missing channel id");
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (body === undefined || body.trim() === "") {
|
|
226
|
-
throw new Error("missing message body");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
if (extra.length > 0) {
|
|
230
|
-
throw new Error("linzumi channel post accepts channel id and one message body");
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
return {
|
|
234
|
-
kind: "channelPost",
|
|
235
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
236
|
-
channelId,
|
|
237
|
-
body,
|
|
238
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
case "post": {
|
|
242
|
-
const parsed = parseAgentArgs(rest);
|
|
243
|
-
const [threadId, body, ...extra] = parsed.positionals;
|
|
244
|
-
|
|
245
|
-
if (threadId === undefined || threadId.trim() === "") {
|
|
246
|
-
throw new Error("missing thread id");
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
if (body === undefined || body.trim() === "") {
|
|
250
|
-
throw new Error("missing message body");
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (extra.length > 0) {
|
|
254
|
-
throw new Error("linzumi post accepts thread id and one message body");
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
return {
|
|
258
|
-
kind: "post",
|
|
259
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
260
|
-
threadId,
|
|
261
|
-
body,
|
|
262
|
-
messageKind: parsePostKind(parsed.flags.get("kind")),
|
|
263
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
case "inbox": {
|
|
267
|
-
const parsed = parseAgentArgs(rest);
|
|
268
|
-
const [threadId, ...extra] = parsed.positionals;
|
|
269
|
-
|
|
270
|
-
if (threadId === undefined || threadId.trim() === "") {
|
|
271
|
-
throw new Error("missing thread id");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (extra.length > 0) {
|
|
275
|
-
throw new Error("linzumi inbox accepts one thread id");
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return {
|
|
279
|
-
kind: "inbox",
|
|
280
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
281
|
-
threadId,
|
|
282
|
-
sinceLast: parsed.flags.get("since-last") === "true",
|
|
283
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
case "done": {
|
|
287
|
-
const parsed = parseAgentArgs(rest);
|
|
288
|
-
const [threadId, ...extra] = parsed.positionals;
|
|
289
|
-
|
|
290
|
-
if (threadId === undefined || threadId.trim() === "") {
|
|
291
|
-
throw new Error("missing thread id");
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
if (extra.length > 0) {
|
|
295
|
-
throw new Error("linzumi done accepts one thread id");
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return {
|
|
299
|
-
kind: "done",
|
|
300
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
301
|
-
threadId,
|
|
302
|
-
message: requiredFlag(parsed.flags, "message"),
|
|
303
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
case "codex": {
|
|
307
|
-
const [subcommand, ...subcommandArgs] = rest;
|
|
308
|
-
|
|
309
|
-
if (subcommand !== "start") {
|
|
310
|
-
throw new Error("linzumi codex supports: start");
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
const parsed = parseAgentArgs(subcommandArgs);
|
|
314
|
-
const [threadId, ...extra] = parsed.positionals;
|
|
315
|
-
|
|
316
|
-
if (threadId === undefined || threadId.trim() === "") {
|
|
317
|
-
throw new Error("missing thread id");
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (extra.length > 0) {
|
|
321
|
-
throw new Error("linzumi codex start accepts one thread id");
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return {
|
|
325
|
-
kind: "codexStart",
|
|
326
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
327
|
-
threadId,
|
|
328
|
-
runnerId: requiredFlag(parsed.flags, "runner"),
|
|
329
|
-
cwd: requiredFlag(parsed.flags, "cwd"),
|
|
330
|
-
workDescription: requiredFlag(parsed.flags, "work-description"),
|
|
331
|
-
model: optionalFlag(parsed.flags, "model"),
|
|
332
|
-
reasoningEffort: optionalFlag(parsed.flags, "reasoning-effort"),
|
|
333
|
-
approvalPolicy: optionalFlag(parsed.flags, "approval-policy"),
|
|
334
|
-
sandbox: optionalFlag(parsed.flags, "sandbox"),
|
|
335
|
-
fast: parsed.flags.get("fast") === "true",
|
|
336
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
337
|
-
};
|
|
338
|
-
}
|
|
339
|
-
case "editor": {
|
|
340
|
-
const [subcommand, ...subcommandArgs] = rest;
|
|
341
|
-
|
|
342
|
-
if (subcommand !== "open") {
|
|
343
|
-
throw new Error("linzumi editor supports: open");
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
const parsed = parseAgentArgs(subcommandArgs);
|
|
347
|
-
const [threadId, ...extra] = parsed.positionals;
|
|
348
|
-
|
|
349
|
-
if (threadId === undefined || threadId.trim() === "") {
|
|
350
|
-
throw new Error("missing thread id");
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (extra.length > 0) {
|
|
354
|
-
throw new Error("linzumi editor open accepts one thread id");
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
return {
|
|
358
|
-
kind: "editorOpen",
|
|
359
|
-
apiUrl: agentApiUrl(parsed.flags),
|
|
360
|
-
threadId,
|
|
361
|
-
runnerId: requiredFlag(parsed.flags, "runner"),
|
|
362
|
-
cwd: requiredFlag(parsed.flags, "cwd"),
|
|
363
|
-
tokenFile: agentTokenFile(parsed.flags),
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
default:
|
|
367
|
-
throw new Error(`invalid agent command: ${first}`);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
function parseAgentArgs(args: readonly string[]): {
|
|
372
|
-
readonly flags: Map<string, string>;
|
|
373
|
-
readonly positionals: readonly string[];
|
|
374
|
-
} {
|
|
375
|
-
const flags = new Map<string, string>();
|
|
376
|
-
const positionals: string[] = [];
|
|
377
|
-
|
|
378
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
379
|
-
const arg = args[index];
|
|
380
|
-
|
|
381
|
-
if (arg === undefined) {
|
|
382
|
-
continue;
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
if (!arg.startsWith("--")) {
|
|
386
|
-
positionals.push(arg);
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const key = arg.slice(2);
|
|
391
|
-
|
|
392
|
-
if (key === "since-last" || key === "fast") {
|
|
393
|
-
flags.set(key, "true");
|
|
394
|
-
continue;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const next = args[index + 1];
|
|
398
|
-
|
|
399
|
-
if (next === undefined || next.startsWith("--")) {
|
|
400
|
-
throw new Error(`missing value for --${key}`);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
flags.set(key, next);
|
|
404
|
-
index += 1;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return { flags, positionals };
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
function parsePostKind(value: string | undefined): "progress" | "question" {
|
|
411
|
-
switch (value) {
|
|
412
|
-
case undefined:
|
|
413
|
-
case "progress":
|
|
414
|
-
return "progress";
|
|
415
|
-
case "question":
|
|
416
|
-
return "question";
|
|
417
|
-
case "done":
|
|
418
|
-
throw new Error("use linzumi done for done messages");
|
|
419
|
-
default:
|
|
420
|
-
throw new Error("--kind must be progress or question");
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
async function runSignup(
|
|
425
|
-
command: Extract<AgentCommand, { readonly kind: "signup" }>,
|
|
426
|
-
deps: AgentCliDeps,
|
|
427
|
-
): Promise<void> {
|
|
428
|
-
const response = await postJson(
|
|
429
|
-
command.apiUrl,
|
|
430
|
-
"/agent/signup",
|
|
431
|
-
{
|
|
432
|
-
email: command.email,
|
|
433
|
-
agent_name: command.agentName,
|
|
434
|
-
},
|
|
435
|
-
undefined,
|
|
436
|
-
deps.fetchImpl,
|
|
437
|
-
);
|
|
438
|
-
const pendingId = requiredString(response, "pending_id");
|
|
439
|
-
const expiresInSeconds = numberValue(response.expires_in_seconds) ?? 600;
|
|
440
|
-
const codeFormatHint = stringValue(response.code_format_hint) ?? "XXXX-XXXX";
|
|
441
|
-
|
|
442
|
-
deps.stdout.write(`pending_id: ${pendingId}\n`);
|
|
443
|
-
deps.stdout.write(
|
|
444
|
-
`Code emailed to ${command.email} (format ${codeFormatHint}, expires in ${Math.ceil(
|
|
445
|
-
expiresInSeconds / 60,
|
|
446
|
-
)}m)\n`,
|
|
447
|
-
);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
async function runClaim(
|
|
451
|
-
command: Extract<AgentCommand, { readonly kind: "claim" }>,
|
|
452
|
-
deps: AgentCliDeps,
|
|
453
|
-
): Promise<void> {
|
|
454
|
-
const response = await postJson(
|
|
455
|
-
command.apiUrl,
|
|
456
|
-
"/agent/claim",
|
|
457
|
-
{
|
|
458
|
-
pending_id: command.pendingId,
|
|
459
|
-
code: command.code,
|
|
460
|
-
},
|
|
461
|
-
undefined,
|
|
462
|
-
deps.fetchImpl,
|
|
463
|
-
);
|
|
464
|
-
const tokenFile: AgentTokenFile = {
|
|
465
|
-
apiUrl: command.apiUrl,
|
|
466
|
-
agentToken: requiredString(response, "agent_token"),
|
|
467
|
-
agentId: requiredString(response, "agent_id"),
|
|
468
|
-
workspaceId: requiredString(response, "workspace_id"),
|
|
469
|
-
channelId: stringValue(response.channel_id),
|
|
470
|
-
ownerUsername: stringValue(response.owner_username),
|
|
471
|
-
channelUrl: requiredString(response, "channel_url"),
|
|
472
|
-
loginUrl: requiredString(response, "login_url"),
|
|
473
|
-
supportChannelId: requiredString(response, "support_channel_id"),
|
|
474
|
-
supportChannelUrl: requiredString(response, "support_channel_url"),
|
|
475
|
-
savedAt: new Date().toISOString(),
|
|
476
|
-
cursors: {},
|
|
477
|
-
};
|
|
478
|
-
|
|
479
|
-
writeTokenFile(command.tokenFile, tokenFile, deps.writeTextFile);
|
|
480
|
-
deps.stdout.write(`Logged in as agent ${tokenFile.agentId}\n`);
|
|
481
|
-
deps.stdout.write(`login_url: ${tokenFile.loginUrl}\n`);
|
|
482
|
-
deps.stdout.write(`channel_url: ${tokenFile.channelUrl}\n`);
|
|
483
|
-
deps.stdout.write(`support_channel_id: ${tokenFile.supportChannelId}\n`);
|
|
484
|
-
deps.stdout.write(`support_channel_url: ${tokenFile.supportChannelUrl}\n`);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
async function runThreadNew(
|
|
488
|
-
command: Extract<AgentCommand, { readonly kind: "threadNew" }>,
|
|
489
|
-
deps: AgentCliDeps,
|
|
490
|
-
): Promise<void> {
|
|
491
|
-
const tokenFile = readTokenFile(command.tokenFile, deps.readTextFile);
|
|
492
|
-
const response = await postJson(
|
|
493
|
-
command.apiUrl,
|
|
494
|
-
"/agent/threads",
|
|
495
|
-
{
|
|
496
|
-
title: command.title,
|
|
497
|
-
first_message: command.message,
|
|
498
|
-
},
|
|
499
|
-
tokenFile.agentToken,
|
|
500
|
-
deps.fetchImpl,
|
|
501
|
-
);
|
|
502
|
-
|
|
503
|
-
deps.stdout.write(`thread_id: ${requiredString(response, "thread_id")}\n`);
|
|
504
|
-
deps.stdout.write(`thread_url: ${requiredString(response, "thread_url")}\n`);
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
async function runChannelPost(
|
|
508
|
-
command: Extract<AgentCommand, { readonly kind: "channelPost" }>,
|
|
509
|
-
deps: AgentCliDeps,
|
|
510
|
-
): Promise<void> {
|
|
511
|
-
const tokenFile = readTokenFile(command.tokenFile, deps.readTextFile);
|
|
512
|
-
const response = await postJson(
|
|
513
|
-
command.apiUrl,
|
|
514
|
-
`/agent/channels/${encodeURIComponent(command.channelId)}/messages`,
|
|
515
|
-
{ body: command.body },
|
|
516
|
-
tokenFile.agentToken,
|
|
517
|
-
deps.fetchImpl,
|
|
518
|
-
);
|
|
519
|
-
|
|
520
|
-
deps.stdout.write(`message_id: ${requiredString(response, "message_id")}\n`);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
async function runPost(
|
|
524
|
-
command: Extract<AgentCommand, { readonly kind: "post" }>,
|
|
525
|
-
deps: AgentCliDeps,
|
|
526
|
-
): Promise<void> {
|
|
527
|
-
const response = await createThreadMessage(
|
|
528
|
-
command.apiUrl,
|
|
529
|
-
command.tokenFile,
|
|
530
|
-
command.threadId,
|
|
531
|
-
command.body,
|
|
532
|
-
command.messageKind,
|
|
533
|
-
deps,
|
|
534
|
-
);
|
|
535
|
-
|
|
536
|
-
deps.stdout.write(`message_id: ${requiredString(response, "message_id")}\n`);
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
async function runDone(
|
|
540
|
-
command: Extract<AgentCommand, { readonly kind: "done" }>,
|
|
541
|
-
deps: AgentCliDeps,
|
|
542
|
-
): Promise<void> {
|
|
543
|
-
const response = await createThreadMessage(
|
|
544
|
-
command.apiUrl,
|
|
545
|
-
command.tokenFile,
|
|
546
|
-
command.threadId,
|
|
547
|
-
command.message,
|
|
548
|
-
"done",
|
|
549
|
-
deps,
|
|
550
|
-
);
|
|
551
|
-
|
|
552
|
-
deps.stdout.write(`message_id: ${requiredString(response, "message_id")}\n`);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
async function runCodexStart(
|
|
556
|
-
command: Extract<AgentCommand, { readonly kind: "codexStart" }>,
|
|
557
|
-
deps: AgentCliDeps,
|
|
558
|
-
): Promise<void> {
|
|
559
|
-
const tokenFile = readTokenFile(command.tokenFile, deps.readTextFile);
|
|
560
|
-
const body: JsonObject = {
|
|
561
|
-
runner_id: command.runnerId,
|
|
562
|
-
cwd: command.cwd,
|
|
563
|
-
work_description: command.workDescription,
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
putOptional(body, "model", command.model);
|
|
567
|
-
putOptional(body, "reasoning_effort", command.reasoningEffort);
|
|
568
|
-
putOptional(body, "approval_policy", command.approvalPolicy);
|
|
569
|
-
putOptional(body, "sandbox", command.sandbox);
|
|
570
|
-
|
|
571
|
-
if (command.fast) {
|
|
572
|
-
body.fast = true;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
const response = await postJson(
|
|
576
|
-
command.apiUrl,
|
|
577
|
-
`/agent/threads/${encodeURIComponent(command.threadId)}/codex/start`,
|
|
578
|
-
body,
|
|
579
|
-
tokenFile.agentToken,
|
|
580
|
-
deps.fetchImpl,
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
deps.stdout.write(`codex_status: ${requiredString(response, "status")}\n`);
|
|
584
|
-
deps.stdout.write(`runner_id: ${requiredString(response, "runner_id")}\n`);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
async function runEditorOpen(
|
|
588
|
-
command: Extract<AgentCommand, { readonly kind: "editorOpen" }>,
|
|
589
|
-
deps: AgentCliDeps,
|
|
590
|
-
): Promise<void> {
|
|
591
|
-
const tokenFile = readTokenFile(command.tokenFile, deps.readTextFile);
|
|
592
|
-
const response = await postJson(
|
|
593
|
-
command.apiUrl,
|
|
594
|
-
`/agent/threads/${encodeURIComponent(command.threadId)}/editor/open`,
|
|
595
|
-
{
|
|
596
|
-
runner_id: command.runnerId,
|
|
597
|
-
cwd: command.cwd,
|
|
598
|
-
},
|
|
599
|
-
tokenFile.agentToken,
|
|
600
|
-
deps.fetchImpl,
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
deps.stdout.write(`editor_status: ${requiredString(response, "status")}\n`);
|
|
604
|
-
deps.stdout.write(`editor_url: ${requiredString(response, "editor_url")}\n`);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
async function createThreadMessage(
|
|
608
|
-
apiUrl: string,
|
|
609
|
-
tokenFilePath: string,
|
|
610
|
-
threadId: string,
|
|
611
|
-
body: string,
|
|
612
|
-
kind: "progress" | "question" | "done",
|
|
613
|
-
deps: AgentCliDeps,
|
|
614
|
-
): Promise<JsonObject> {
|
|
615
|
-
const tokenFile = readTokenFile(tokenFilePath, deps.readTextFile);
|
|
616
|
-
|
|
617
|
-
return await postJson(
|
|
618
|
-
apiUrl,
|
|
619
|
-
`/agent/threads/${encodeURIComponent(threadId)}/messages`,
|
|
620
|
-
{ body, kind },
|
|
621
|
-
tokenFile.agentToken,
|
|
622
|
-
deps.fetchImpl,
|
|
623
|
-
);
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
async function runInbox(
|
|
627
|
-
command: Extract<AgentCommand, { readonly kind: "inbox" }>,
|
|
628
|
-
deps: AgentCliDeps,
|
|
629
|
-
): Promise<void> {
|
|
630
|
-
const tokenFile = readTokenFile(command.tokenFile, deps.readTextFile);
|
|
631
|
-
const previousCursor = command.sinceLast
|
|
632
|
-
? tokenFile.cursors[command.threadId]
|
|
633
|
-
: undefined;
|
|
634
|
-
const url = new URL(
|
|
635
|
-
`/agent/threads/${encodeURIComponent(command.threadId)}/messages`,
|
|
636
|
-
normalizedApiUrl(command.apiUrl),
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
if (previousCursor !== undefined) {
|
|
640
|
-
url.searchParams.set("since", previousCursor);
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const response = await fetchJson(url, {
|
|
644
|
-
method: "GET",
|
|
645
|
-
headers: authorizationHeaders(tokenFile.agentToken),
|
|
646
|
-
}, deps.fetchImpl);
|
|
647
|
-
const messages = arrayValue(response.messages) ?? [];
|
|
648
|
-
const cursor = stringValue(response.cursor);
|
|
649
|
-
|
|
650
|
-
messages.forEach((entry) => {
|
|
651
|
-
const message = objectValue(entry);
|
|
652
|
-
const author = objectValue(message?.author);
|
|
653
|
-
const displayName =
|
|
654
|
-
stringValue(author?.display_name) ?? stringValue(author?.kind) ?? "unknown";
|
|
655
|
-
const body = stringValue(message?.body) ?? "";
|
|
656
|
-
|
|
657
|
-
deps.stdout.write(`[${displayName}] ${body}\n`);
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
if (cursor !== undefined) {
|
|
661
|
-
writeTokenFile(
|
|
662
|
-
command.tokenFile,
|
|
663
|
-
{
|
|
664
|
-
...tokenFile,
|
|
665
|
-
cursors: {
|
|
666
|
-
...tokenFile.cursors,
|
|
667
|
-
[command.threadId]: cursor,
|
|
668
|
-
},
|
|
669
|
-
},
|
|
670
|
-
deps.writeTextFile,
|
|
671
|
-
);
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
async function postJson(
|
|
676
|
-
apiUrl: string,
|
|
677
|
-
path: string,
|
|
678
|
-
body: JsonObject,
|
|
679
|
-
token: string | undefined,
|
|
680
|
-
fetchImpl: typeof fetch,
|
|
681
|
-
): Promise<JsonObject> {
|
|
682
|
-
const url = new URL(path, normalizedApiUrl(apiUrl));
|
|
683
|
-
const headers =
|
|
684
|
-
token === undefined
|
|
685
|
-
? { "content-type": "application/json" }
|
|
686
|
-
: { ...authorizationHeaders(token), "content-type": "application/json" };
|
|
687
|
-
|
|
688
|
-
return await fetchJson(
|
|
689
|
-
url,
|
|
690
|
-
{
|
|
691
|
-
method: "POST",
|
|
692
|
-
headers,
|
|
693
|
-
body: JSON.stringify(body),
|
|
694
|
-
},
|
|
695
|
-
fetchImpl,
|
|
696
|
-
);
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
async function fetchJson(
|
|
700
|
-
url: URL,
|
|
701
|
-
init: RequestInit,
|
|
702
|
-
fetchImpl: typeof fetch,
|
|
703
|
-
): Promise<JsonObject> {
|
|
704
|
-
const response = await fetchImpl(url, init);
|
|
705
|
-
const text = await response.text();
|
|
706
|
-
const payload = parseJsonObject(text);
|
|
707
|
-
|
|
708
|
-
if (!response.ok) {
|
|
709
|
-
const error = objectValue(payload.error);
|
|
710
|
-
const code = stringValue(error?.code) ?? `http_${response.status}`;
|
|
711
|
-
const message = stringValue(error?.message) ?? response.statusText;
|
|
712
|
-
throw new Error(`${code}: ${message}`);
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
return payload;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function parseJsonObject(text: string): JsonObject {
|
|
719
|
-
try {
|
|
720
|
-
const parsed: unknown = JSON.parse(text);
|
|
721
|
-
|
|
722
|
-
if (isJsonObject(parsed)) {
|
|
723
|
-
return parsed;
|
|
724
|
-
}
|
|
725
|
-
} catch (_error) {
|
|
726
|
-
throw new Error("expected JSON object response");
|
|
727
|
-
}
|
|
728
|
-
|
|
729
|
-
throw new Error("expected JSON object response");
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
function requiredFlag(flags: Map<string, string>, key: string): string {
|
|
733
|
-
const value = flags.get(key);
|
|
734
|
-
|
|
735
|
-
if (value === undefined || value.trim() === "") {
|
|
736
|
-
throw new Error(`missing --${key}`);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
return value.trim();
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
function optionalFlag(flags: Map<string, string>, key: string): string | undefined {
|
|
743
|
-
const value = flags.get(key);
|
|
744
|
-
return value === undefined || value.trim() === "" ? undefined : value.trim();
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function putOptional(object: JsonObject, key: string, value: string | undefined): void {
|
|
748
|
-
if (value !== undefined) {
|
|
749
|
-
object[key] = value;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
|
|
753
|
-
function requiredString(object: JsonObject, key: string): string {
|
|
754
|
-
const value = stringValue(object[key]);
|
|
755
|
-
|
|
756
|
-
if (value === undefined) {
|
|
757
|
-
throw new Error(`missing ${key} in response`);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return value;
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
function numberValue(value: JsonValue | undefined): number | undefined {
|
|
764
|
-
return typeof value === "number" && Number.isFinite(value)
|
|
765
|
-
? value
|
|
766
|
-
: undefined;
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
function agentApiUrl(flags: Map<string, string>): string {
|
|
770
|
-
return flags.get("api-url") ?? defaultApiUrl;
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
function agentTokenFile(flags: Map<string, string>): string {
|
|
774
|
-
return flags.get("agent-token-file") ?? defaultAgentTokenFilePath();
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
export function defaultAgentTokenFilePath(): string {
|
|
778
|
-
return join(homedir(), ".linzumi", "agent-token.json");
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function normalizedApiUrl(apiUrl: string): string {
|
|
782
|
-
return apiUrl.endsWith("/") ? apiUrl : `${apiUrl}/`;
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
function authorizationHeaders(token: string): HeadersInit {
|
|
786
|
-
return { authorization: `Bearer ${token}` };
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function readOptionalTextFile(path: string): string | undefined {
|
|
790
|
-
return existsSync(path) ? readFileSync(path, "utf8") : undefined;
|
|
791
|
-
}
|
|
792
|
-
|
|
793
|
-
function writeTextFile(path: string, content: string): void {
|
|
794
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
795
|
-
writeFileSync(path, content);
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
export function readStoredAgentTokenFile(
|
|
799
|
-
path: string,
|
|
800
|
-
readTextFile: AgentCliDeps["readTextFile"] = readOptionalTextFile,
|
|
801
|
-
): AgentTokenFile {
|
|
802
|
-
const content = readTextFile(path);
|
|
803
|
-
|
|
804
|
-
if (content === undefined) {
|
|
805
|
-
throw new Error(`missing agent token file: ${path}`);
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
const parsed = parseJsonObject(content);
|
|
809
|
-
const cursorsValue = objectValue(parsed.cursors);
|
|
810
|
-
const cursors =
|
|
811
|
-
cursorsValue === undefined
|
|
812
|
-
? {}
|
|
813
|
-
: Object.fromEntries(
|
|
814
|
-
Object.entries(cursorsValue).flatMap(([key, value]) => {
|
|
815
|
-
const cursor = stringValue(value);
|
|
816
|
-
return cursor === undefined ? [] : [[key, cursor]];
|
|
817
|
-
}),
|
|
818
|
-
);
|
|
819
|
-
|
|
820
|
-
return {
|
|
821
|
-
apiUrl: requiredString(parsed, "apiUrl"),
|
|
822
|
-
agentToken: requiredString(parsed, "agentToken"),
|
|
823
|
-
agentId: requiredString(parsed, "agentId"),
|
|
824
|
-
workspaceId: requiredString(parsed, "workspaceId"),
|
|
825
|
-
channelId: stringValue(parsed.channelId),
|
|
826
|
-
ownerUsername: stringValue(parsed.ownerUsername),
|
|
827
|
-
channelUrl: requiredString(parsed, "channelUrl"),
|
|
828
|
-
loginUrl: requiredString(parsed, "loginUrl"),
|
|
829
|
-
supportChannelId: requiredString(parsed, "supportChannelId"),
|
|
830
|
-
supportChannelUrl: stringValue(parsed.supportChannelUrl),
|
|
831
|
-
savedAt: requiredString(parsed, "savedAt"),
|
|
832
|
-
cursors,
|
|
833
|
-
};
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function readTokenFile(
|
|
837
|
-
path: string,
|
|
838
|
-
readTextFile: AgentCliDeps["readTextFile"],
|
|
839
|
-
): AgentTokenFile {
|
|
840
|
-
return readStoredAgentTokenFile(path, readTextFile);
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
function writeTokenFile(
|
|
844
|
-
path: string,
|
|
845
|
-
tokenFile: AgentTokenFile,
|
|
846
|
-
writeTextFile: AgentCliDeps["writeTextFile"],
|
|
847
|
-
): void {
|
|
848
|
-
writeTextFile(path, `${JSON.stringify(tokenFile, null, 2)}\n`);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
function agentHelpText(): string {
|
|
852
|
-
return `Linzumi agent bootstrap
|
|
853
|
-
|
|
854
|
-
Usage:
|
|
855
|
-
linzumi signup --email <email> --agent-name <name> [--api-url <url>]
|
|
856
|
-
linzumi claim --pending <pending_id> --code <XXXX-XXXX> [--api-url <url>]
|
|
857
|
-
linzumi thread new <title> --message <message> [--api-url <url>]
|
|
858
|
-
linzumi channel post <channel_id> <message> [--api-url <url>]
|
|
859
|
-
linzumi post <thread_id> <message> [--kind progress|question]
|
|
860
|
-
linzumi inbox <thread_id> --since-last
|
|
861
|
-
linzumi done <thread_id> --message <message>
|
|
862
|
-
linzumi codex start <thread_id> --runner <runner_id> --cwd <path> --work-description <text>
|
|
863
|
-
linzumi editor open <thread_id> --runner <runner_id> --cwd <path>
|
|
864
|
-
|
|
865
|
-
Options:
|
|
866
|
-
--api-url <url> Agent API origin, default ${defaultApiUrl}
|
|
867
|
-
--agent-token-file <path> Token cache path, default ~/.linzumi/agent-token.json
|
|
868
|
-
|
|
869
|
-
Launch target:
|
|
870
|
-
zero-to-hello-world-pr+editor in under 3 minutes.
|
|
871
|
-
`;
|
|
872
|
-
}
|