@duetso/agent 0.1.34 → 0.1.36

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.
Files changed (92) hide show
  1. package/dist/package.json +2 -2
  2. package/dist/src/cli/env.d.ts +18 -0
  3. package/dist/src/cli/env.d.ts.map +1 -0
  4. package/dist/src/cli/env.js +114 -0
  5. package/dist/src/cli/env.js.map +1 -0
  6. package/dist/src/cli/help.d.ts +8 -0
  7. package/dist/src/cli/help.d.ts.map +1 -0
  8. package/dist/src/cli/help.js +178 -0
  9. package/dist/src/cli/help.js.map +1 -0
  10. package/dist/src/cli/login.d.ts +13 -0
  11. package/dist/src/cli/login.d.ts.map +1 -0
  12. package/dist/src/cli/login.js +61 -0
  13. package/dist/src/cli/login.js.map +1 -0
  14. package/dist/src/cli/memory-db.d.ts +24 -0
  15. package/dist/src/cli/memory-db.d.ts.map +1 -0
  16. package/dist/src/cli/memory-db.js +74 -0
  17. package/dist/src/cli/memory-db.js.map +1 -0
  18. package/dist/src/cli/memory-tui.d.ts +11 -0
  19. package/dist/src/cli/memory-tui.d.ts.map +1 -0
  20. package/dist/src/cli/memory-tui.js +266 -0
  21. package/dist/src/cli/memory-tui.js.map +1 -0
  22. package/dist/src/cli/memory.d.ts +9 -0
  23. package/dist/src/cli/memory.d.ts.map +1 -0
  24. package/dist/src/cli/memory.js +38 -0
  25. package/dist/src/cli/memory.js.map +1 -0
  26. package/dist/src/cli/package-manager.d.ts +38 -0
  27. package/dist/src/cli/package-manager.d.ts.map +1 -0
  28. package/dist/src/cli/package-manager.js +78 -0
  29. package/dist/src/cli/package-manager.js.map +1 -0
  30. package/dist/src/cli/resume-hint.d.ts +22 -0
  31. package/dist/src/cli/resume-hint.d.ts.map +1 -0
  32. package/dist/src/cli/resume-hint.js +61 -0
  33. package/dist/src/cli/resume-hint.js.map +1 -0
  34. package/dist/src/cli/run.d.ts +43 -0
  35. package/dist/src/cli/run.d.ts.map +1 -0
  36. package/dist/src/cli/run.js +273 -0
  37. package/dist/src/cli/run.js.map +1 -0
  38. package/dist/src/cli/shared.d.ts +55 -0
  39. package/dist/src/cli/shared.d.ts.map +1 -0
  40. package/dist/src/cli/shared.js +125 -0
  41. package/dist/src/cli/shared.js.map +1 -0
  42. package/dist/src/cli/skills.d.ts +10 -0
  43. package/dist/src/cli/skills.d.ts.map +1 -0
  44. package/dist/src/cli/skills.js +42 -0
  45. package/dist/src/cli/skills.js.map +1 -0
  46. package/dist/src/cli/upgrade.d.ts +9 -0
  47. package/dist/src/cli/upgrade.d.ts.map +1 -0
  48. package/dist/src/cli/upgrade.js +52 -0
  49. package/dist/src/cli/upgrade.js.map +1 -0
  50. package/dist/src/cli/version-check.d.ts +22 -0
  51. package/dist/src/cli/version-check.d.ts.map +1 -0
  52. package/dist/src/cli/version-check.js +78 -0
  53. package/dist/src/cli/version-check.js.map +1 -0
  54. package/dist/src/cli.d.ts +20 -63
  55. package/dist/src/cli.d.ts.map +1 -1
  56. package/dist/src/cli.js +38 -887
  57. package/dist/src/cli.js.map +1 -1
  58. package/dist/src/memory/observational-prompts.d.ts.map +1 -1
  59. package/dist/src/memory/observational-prompts.js +11 -7
  60. package/dist/src/memory/observational-prompts.js.map +1 -1
  61. package/dist/src/tui/app.d.ts +7 -47
  62. package/dist/src/tui/app.d.ts.map +1 -1
  63. package/dist/src/tui/app.js +204 -389
  64. package/dist/src/tui/app.js.map +1 -1
  65. package/dist/src/tui/autocomplete.d.ts +91 -0
  66. package/dist/src/tui/autocomplete.d.ts.map +1 -0
  67. package/dist/src/tui/autocomplete.js +177 -0
  68. package/dist/src/tui/autocomplete.js.map +1 -0
  69. package/dist/src/tui/file-index.d.ts +11 -0
  70. package/dist/src/tui/file-index.d.ts.map +1 -0
  71. package/dist/src/tui/file-index.js +75 -0
  72. package/dist/src/tui/file-index.js.map +1 -0
  73. package/dist/src/tui/history.d.ts +50 -0
  74. package/dist/src/tui/history.d.ts.map +1 -0
  75. package/dist/src/tui/history.js +132 -0
  76. package/dist/src/tui/history.js.map +1 -0
  77. package/dist/src/tui/sidebar.d.ts +20 -0
  78. package/dist/src/tui/sidebar.d.ts.map +1 -0
  79. package/dist/src/tui/sidebar.js +118 -0
  80. package/dist/src/tui/sidebar.js.map +1 -0
  81. package/dist/src/tui/theme.d.ts +15 -0
  82. package/dist/src/tui/theme.d.ts.map +1 -0
  83. package/dist/src/tui/theme.js +18 -0
  84. package/dist/src/tui/theme.js.map +1 -0
  85. package/dist/src/turn-runner/prompts.d.ts.map +1 -1
  86. package/dist/src/turn-runner/prompts.js +10 -1
  87. package/dist/src/turn-runner/prompts.js.map +1 -1
  88. package/dist/src/turn-runner/tools.d.ts +15 -1
  89. package/dist/src/turn-runner/tools.d.ts.map +1 -1
  90. package/dist/src/turn-runner/tools.js +96 -11
  91. package/dist/src/turn-runner/tools.js.map +1 -1
  92. package/package.json +2 -2
package/dist/src/cli.js CHANGED
@@ -1,39 +1,35 @@
1
1
  #!/usr/bin/env bun
2
2
  /**
3
- * duet CLI
3
+ * duet CLI entry point.
4
4
  *
5
5
  * Usage:
6
6
  * duet "build a todo app in React"
7
7
  * duet --model opus-4.7 "refactor auth system"
8
8
  * echo "fix the bug in server.ts" | duet
9
+ *
10
+ * The actual command implementations live under `src/cli/`. This file is the
11
+ * subcommand dispatcher and the public re-export surface for tests and
12
+ * callers that historically imported helpers from `src/cli.ts`.
9
13
  */
10
- import { spawn } from "node:child_process";
11
- import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
12
- import { homedir } from "node:os";
13
- import { basename, dirname, isAbsolute, join, resolve } from "node:path";
14
- import { createInterface } from "node:readline/promises";
15
- import { fileURLToPath, pathToFileURL } from "node:url";
16
- import dotenv from "dotenv";
14
+ import { pathToFileURL } from "node:url";
17
15
  import packageJson from "../package.json" with { type: "json" };
16
+ import { runEnvCommand } from "./cli/env.js";
17
+ import { runLoginCommand } from "./cli/login.js";
18
+ import { runMemoryCommand } from "./cli/memory.js";
19
+ import { runRunCommand } from "./cli/run.js";
20
+ import { runSkillsCommand } from "./cli/skills.js";
21
+ import { runUpgradeCommand } from "./cli/upgrade.js";
18
22
  import { shimDuetApiKeyToAiGateway } from "./model-resolution/duet-gateway.js";
19
- import { resolveDuetAppBaseUrl } from "./lib/duet-app-url.js";
20
- import { loginWithBrowser } from "./lib/login.js";
21
- import { maybeAutoSyncDefaultSkills, syncDefaultSkills } from "./lib/sync-skills.js";
22
- import { describeModelResolution, resolveCliMemoryModel, resolveCliModel, } from "./model-resolution/resolver.js";
23
- import { SessionManager } from "./session/session-manager.js";
24
- import { discoverInstalledSkills, resolveSkillScope } from "./turn-runner/skills.js";
25
- import { runTui } from "./tui/app.js";
26
- const VERSION_CHECK_TIMEOUT_MS = 1_500;
27
- const DEFAULT_RESUME_HISTORY_LINES = 40;
28
- const PACKAGE_MANAGERS = ["npm", "bun", "pnpm", "yarn"];
29
- const DEFAULT_DUET_ENV_FILE = "~/.duet/.env";
30
- const SUPPORTED_API_KEYS = [
31
- "DUET_API_KEY",
32
- "ANTHROPIC_API_KEY",
33
- "AI_GATEWAY_API_KEY",
34
- "OPENROUTER_API_KEY",
35
- "OPENAI_API_KEY",
36
- ];
23
+ export { buildCliTurnConfig, runRunCommand, shouldUseTui } from "./cli/run.js";
24
+ export { runEnvCommand } from "./cli/env.js";
25
+ export { runLoginCommand } from "./cli/login.js";
26
+ export { runSkillsCommand } from "./cli/skills.js";
27
+ export { runUpgradeCommand } from "./cli/upgrade.js";
28
+ export { runMemoryCommand } from "./cli/memory.js";
29
+ export { cliEnvFilePaths, defaultDuetEnvFilePath, fileExists, formatEnvEntries, loadCliEnvFiles, parseResumeHistoryLines, resolveUserPath, shellQuote, } from "./cli/shared.js";
30
+ export { detectPackageManagerFromContext, globalUpgradeCommand } from "./cli/package-manager.js";
31
+ export { compareSemverVersions, fetchLatestPackageVersion, formatNewVersionNotice, getNewVersionNotice, } from "./cli/version-check.js";
32
+ export { resumeCommand } from "./cli/resume-hint.js";
37
33
  const PACKAGE_METADATA = {
38
34
  name: packageJson.name,
39
35
  version: packageJson.version,
@@ -44,881 +40,36 @@ async function main() {
44
40
  // explicit AI_GATEWAY_API_KEY wins.
45
41
  shimDuetApiKeyToAiGateway();
46
42
  const args = process.argv.slice(2);
47
- if (args[0] === "upgrade") {
48
- try {
49
- await runUpgradeCommand(args.slice(1));
50
- }
51
- catch (err) {
52
- console.error(`Fatal: ${err.message}`);
53
- process.exitCode = 1;
43
+ const subcommand = args[0];
44
+ try {
45
+ if (subcommand === "upgrade") {
46
+ await runUpgradeCommand(args.slice(1), PACKAGE_METADATA.name);
47
+ return;
54
48
  }
55
- return;
56
- }
57
- if (args[0] === "skills") {
58
- try {
49
+ if (subcommand === "skills") {
59
50
  runSkillsCommand(args.slice(1));
51
+ return;
60
52
  }
61
- catch (err) {
62
- console.error(`Fatal: ${err.message}`);
63
- process.exitCode = 1;
64
- }
65
- return;
66
- }
67
- if (args[0] === "env") {
68
- try {
53
+ if (subcommand === "env") {
69
54
  await runEnvCommand(args.slice(1));
55
+ return;
70
56
  }
71
- catch (err) {
72
- console.error(`Fatal: ${err.message}`);
73
- process.exitCode = 1;
74
- }
75
- return;
76
- }
77
- if (args[0] === "login") {
78
- try {
57
+ if (subcommand === "login") {
79
58
  await runLoginCommand(args.slice(1));
59
+ return;
80
60
  }
81
- catch (err) {
82
- console.error(`Fatal: ${err.message}`);
83
- process.exitCode = 1;
84
- }
85
- return;
86
- }
87
- // Parse flags
88
- let modelName;
89
- let memoryModelName;
90
- let workDir = process.cwd();
91
- let resumeSessionId;
92
- let systemInstructions;
93
- let systemPromptFiles;
94
- let resumeHistoryLines = DEFAULT_RESUME_HISTORY_LINES;
95
- let resumeHistoryLinesExplicit = false;
96
- let jsonOutput = false;
97
- let envFilePath;
98
- let disableDurableMemory = false;
99
- const promptParts = [];
100
- const interactive = Boolean(process.stdin.isTTY ?? process.stdout.isTTY);
101
- for (let i = 0; i < args.length; i++) {
102
- switch (args[i]) {
103
- case "--model":
104
- case "-m":
105
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
106
- fail(`Missing value for ${args[i]}`);
107
- modelName = args[++i];
108
- break;
109
- case "--memory-model":
110
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
111
- fail(`Missing value for ${args[i]}`);
112
- memoryModelName = args[++i];
113
- break;
114
- case "--no-memory":
115
- disableDurableMemory = true;
116
- break;
117
- case "--workdir":
118
- case "-w":
119
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
120
- fail(`Missing value for ${args[i]}`);
121
- workDir = args[++i];
122
- break;
123
- case "--resume":
124
- case "-r":
125
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
126
- fail(`Missing value for ${args[i]}`);
127
- resumeSessionId = args[++i];
128
- break;
129
- case "--resume-history-lines":
130
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
131
- fail(`Missing value for ${args[i]}`);
132
- try {
133
- resumeHistoryLines = parseResumeHistoryLines(args[++i], args[i - 1]);
134
- }
135
- catch (error) {
136
- fail(error instanceof Error ? error.message : String(error));
137
- }
138
- resumeHistoryLinesExplicit = true;
139
- break;
140
- case "--system-prompt":
141
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
142
- fail(`Missing value for ${args[i]}`);
143
- systemInstructions = args[++i];
144
- break;
145
- case "--system-prompt-file":
146
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
147
- fail(`Missing value for ${args[i]}`);
148
- systemPromptFiles = [...(systemPromptFiles ?? []), args[++i]];
149
- break;
150
- case "--no-system-prompt-files":
151
- systemPromptFiles = [];
152
- break;
153
- case "--json":
154
- jsonOutput = true;
155
- break;
156
- case "--env-file":
157
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
158
- fail(`Missing value for ${args[i]}`);
159
- envFilePath = args[++i];
160
- break;
161
- case "--version":
162
- case "-v": {
163
- console.log(PACKAGE_METADATA.version);
164
- process.exit(0);
165
- }
166
- case "--help":
167
- case "-h":
168
- printHelp(PACKAGE_METADATA.name);
169
- process.exit(0);
170
- default:
171
- if (args[i]?.startsWith("-")) {
172
- fail(`Unknown option: ${args[i]}`);
173
- }
174
- promptParts.push(args[i]);
175
- }
176
- }
177
- let prompt = promptParts.join(" ");
178
- // Read from stdin if no prompt is provided
179
- if (!prompt && !interactive) {
180
- const chunks = [];
181
- for await (const chunk of process.stdin) {
182
- chunks.push(chunk);
183
- }
184
- prompt = Buffer.concat(chunks).toString("utf-8").trim();
185
- }
186
- if (!prompt && jsonOutput && interactive) {
187
- prompt = await readInteractivePrompt();
188
- }
189
- if (!prompt && !resumeSessionId && !interactive) {
190
- console.error("Usage: duet <prompt>");
191
- console.error(' e.g., duet "build a todo app"');
192
- process.exit(1);
193
- }
194
- const dotenvKeys = loadCliEnvFiles(workDir, envFilePath);
195
- shimDuetApiKeyToAiGateway();
196
- // Refresh the gateway-managed default skills when the user has previously
197
- // opted in via `duet login` (i.e. `~/.duet/.skills-hash` exists). Logging
198
- // in with --skip-skill-sync leaves no hash, so this stays a no-op until
199
- // the user explicitly syncs at least once. The conditional GET hits 304
200
- // in steady state, so the cost is one cheap round-trip.
201
- if (process.env.DUET_API_KEY) {
202
- await maybeAutoSyncDefaultSkills({ apiKey: process.env.DUET_API_KEY });
203
- }
204
- const { config, modelResolution, memoryModelResolution } = buildCliTurnConfig({
205
- modelName,
206
- memoryModelName,
207
- disableDurableMemory,
208
- workDir,
209
- systemInstructions,
210
- systemPromptFiles,
211
- }, dotenvKeys);
212
- modelName = modelResolution.modelName;
213
- memoryModelName = memoryModelResolution.modelName;
214
- // The CLI has exactly two rendering modes: the interactive TUI, or JSONL
215
- // events. Supplying a prompt selects JSONL so one-shot runs have a stable
216
- // machine-readable contract by default.
217
- const useTui = shouldUseTui({ interactive, jsonOutput, prompt });
218
- const useJson = !useTui;
219
- const newVersionNotice = await getNewVersionNotice();
220
- if (useJson) {
221
- if (newVersionNotice)
222
- process.stderr.write(`${newVersionNotice}\n`);
223
- process.stderr.write(`Model: ${modelName}\n`);
224
- process.stderr.write(`Source: ${describeModelResolution(modelResolution)}\n`);
225
- process.stderr.write(`Memory model: ${memoryModelName}\n`);
226
- process.stderr.write(`Memory source: ${describeModelResolution(memoryModelResolution)}\n`);
227
- }
228
- const manager = new SessionManager(config);
229
- manager.subscribe(({ event }) => {
230
- if (useJson) {
231
- process.stdout.write(`${JSON.stringify(event)}\n`);
232
- }
233
- });
234
- try {
235
- const session = resumeSessionId
236
- ? manager.resume(resumeSessionId)
237
- : manager.create({
238
- ...(config.mode ? { mode: config.mode } : {}),
239
- ...(useTui || !prompt ? {} : { prompt }),
240
- });
241
- let resumedHistory;
242
- if (resumeSessionId) {
243
- // Force-load the persisted state.json so setup hands the resumed
244
- // state to the runner and any TUI history replays before new turns.
245
- await session.hydrate();
246
- if (!session.getState()) {
247
- throw new Error(`Unknown session: ${resumeSessionId}`);
248
- }
249
- // Setup runs against the hydrated state; manager.create() already
250
- // dispatched setup for fresh sessions.
251
- await session.start();
252
- resumedHistory = session.getState()?.agent.messages;
253
- }
254
- if (prompt && resumeSessionId) {
255
- await session.prompt({ message: prompt });
256
- await session.waitForTerminal();
257
- }
258
- else if (prompt && !resumeSessionId) {
259
- await session.waitForTerminal();
260
- }
261
- if (useTui) {
262
- await runTui({
263
- session,
264
- ...(resumedHistory ? { history: resumedHistory } : {}),
265
- resumeHistoryLines,
266
- modelName,
267
- modelSource: describeModelResolution(modelResolution),
268
- memoryModelName,
269
- memoryModelSource: describeModelResolution(memoryModelResolution),
270
- workDir,
271
- sessionId: session.id,
272
- packageVersion: PACKAGE_METADATA.version,
273
- ...(newVersionNotice ? { newVersionNotice } : {}),
274
- });
61
+ if (subcommand === "memory" || subcommand === "memories") {
62
+ await runMemoryCommand(args.slice(1));
63
+ return;
275
64
  }
276
- process.stderr.write(`To resume this session:\n${resumeCommand(session.id, {
277
- modelName,
278
- memoryModelName,
279
- workDir,
280
- disableDurableMemory,
281
- systemInstructions,
282
- systemPromptFiles,
283
- envFilePath,
284
- ...(resumeHistoryLinesExplicit ? { resumeHistoryLines } : {}),
285
- })}\n`);
65
+ await runRunCommand(args, PACKAGE_METADATA);
286
66
  }
287
67
  catch (err) {
288
68
  console.error(`Fatal: ${err.message}`);
289
69
  process.exitCode = 1;
290
70
  }
291
- finally {
292
- await manager.dispose();
293
- }
294
- }
295
- function fail(message) {
296
- console.error(`Fatal: ${message}`);
297
- process.exit(1);
298
- }
299
- export function buildCliTurnConfig(input, dotenvKeys) {
300
- const modelResolution = resolveCliModel(input.modelName, dotenvKeys);
301
- const memoryModelResolution = resolveCliMemoryModel(input.memoryModelName, dotenvKeys);
302
- return {
303
- config: {
304
- model: modelResolution.modelName,
305
- memoryModel: memoryModelResolution.modelName,
306
- ...(input.disableDurableMemory ? { memoryDbPath: false } : {}),
307
- cwd: input.workDir,
308
- ...(input.systemInstructions ? { systemInstructions: input.systemInstructions } : {}),
309
- ...(input.systemPromptFiles ? { systemPromptFiles: input.systemPromptFiles } : {}),
310
- },
311
- modelResolution,
312
- memoryModelResolution,
313
- };
314
- }
315
- export function resolveUserPath(path, baseDir = process.cwd()) {
316
- if (path === "~")
317
- return homedir();
318
- if (path.startsWith("~/"))
319
- return join(homedir(), path.slice(2));
320
- return isAbsolute(path) ? path : resolve(baseDir, path);
321
- }
322
- export function defaultDuetEnvFilePath() {
323
- return resolveUserPath(DEFAULT_DUET_ENV_FILE);
324
- }
325
- export function cliEnvFilePaths(workDir, envFilePath) {
326
- return [
327
- join(workDir, ".env"),
328
- envFilePath ? resolveUserPath(envFilePath, workDir) : defaultDuetEnvFilePath(),
329
- ];
330
- }
331
- export function loadCliEnvFiles(workDir, envFilePath) {
332
- const dotenvKeys = new Set();
333
- for (const path of cliEnvFilePaths(workDir, envFilePath)) {
334
- const result = dotenv.config({ path, quiet: true });
335
- for (const key of Object.keys(result.parsed ?? {})) {
336
- dotenvKeys.add(key);
337
- }
338
- }
339
- return dotenvKeys;
340
- }
341
- export function parseResumeHistoryLines(value, optionName = "--resume-history-lines") {
342
- if (!/^\d+$/.test(value)) {
343
- throw new Error(`${optionName} must be a non-negative integer`);
344
- }
345
- return Number(value);
346
- }
347
- export function shouldUseTui(input) {
348
- return input.interactive && !input.jsonOutput && !input.prompt;
349
- }
350
- async function getNewVersionNotice() {
351
- try {
352
- const latestVersion = await fetchLatestPackageVersion(PACKAGE_METADATA.name);
353
- if (!latestVersion)
354
- return undefined;
355
- if (compareSemverVersions(latestVersion, PACKAGE_METADATA.version) <= 0)
356
- return undefined;
357
- return formatNewVersionNotice(PACKAGE_METADATA.name, PACKAGE_METADATA.version, latestVersion);
358
- }
359
- catch {
360
- // Version checks should never block CLI startup or hide the real command output.
361
- return undefined;
362
- }
363
- }
364
- export function formatNewVersionNotice(packageName, currentVersion, latestVersion) {
365
- return `Update available: ${packageName} ${currentVersion} -> ${latestVersion}. Run: duet upgrade`;
366
- }
367
- async function fetchLatestPackageVersion(packageName) {
368
- const controller = new AbortController();
369
- const timeout = setTimeout(() => controller.abort(), VERSION_CHECK_TIMEOUT_MS);
370
- try {
371
- const metadataUrl = `https://registry.npmjs.org/${packageName.replace("/", "%2F")}`;
372
- const response = await fetch(metadataUrl, { signal: controller.signal });
373
- if (!response.ok)
374
- return undefined;
375
- const metadata = (await response.json());
376
- const latest = metadata["dist-tags"]?.latest;
377
- return typeof latest === "string" ? latest : undefined;
378
- }
379
- finally {
380
- clearTimeout(timeout);
381
- }
382
- }
383
- export function compareSemverVersions(left, right) {
384
- const leftParts = parseSemverVersion(left);
385
- const rightParts = parseSemverVersion(right);
386
- for (let i = 0; i < 3; i++) {
387
- const delta = leftParts.numbers[i] - rightParts.numbers[i];
388
- if (delta !== 0)
389
- return Math.sign(delta);
390
- }
391
- if (leftParts.prerelease === rightParts.prerelease)
392
- return 0;
393
- if (!leftParts.prerelease)
394
- return 1;
395
- if (!rightParts.prerelease)
396
- return -1;
397
- return leftParts.prerelease.localeCompare(rightParts.prerelease);
398
- }
399
- function parseSemverVersion(version) {
400
- const [main = "", prerelease] = version.replace(/^v/, "").split("-", 2);
401
- const [major = "0", minor = "0", patch = "0"] = main.split(".");
402
- return {
403
- numbers: [Number(major) || 0, Number(minor) || 0, Number(patch) || 0],
404
- ...(prerelease ? { prerelease } : {}),
405
- };
406
- }
407
- async function runUpgradeCommand(args) {
408
- let packageManager = detectPackageManager();
409
- let dryRun = false;
410
- const packageName = PACKAGE_METADATA.name;
411
- let targetVersion;
412
- for (let i = 0; i < args.length; i++) {
413
- switch (args[i]) {
414
- case "--manager":
415
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
416
- fail(`Missing value for ${args[i]}`);
417
- packageManager = parsePackageManager(args[++i]);
418
- break;
419
- case "--dry-run":
420
- dryRun = true;
421
- break;
422
- case "--version":
423
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
424
- fail(`Missing value for ${args[i]}`);
425
- targetVersion = normalizePackageVersion(args[++i]);
426
- break;
427
- case "--help":
428
- case "-h":
429
- printUpgradeHelp(packageName);
430
- return;
431
- default:
432
- fail(`Unknown upgrade option: ${args[i]}`);
433
- }
434
- }
435
- targetVersion ??= await fetchLatestPackageVersion(packageName);
436
- if (!targetVersion) {
437
- fail(`Could not resolve latest ${packageName} version from npm`);
438
- }
439
- const command = globalUpgradeCommand(packageManager, packageName, targetVersion);
440
- const commandText = command.map(shellQuote).join(" ");
441
- if (dryRun) {
442
- console.log(commandText);
443
- return;
444
- }
445
- console.error(`Upgrading ${packageName} to ${targetVersion} with ${packageManager}...`);
446
- await runCommand(command[0], command.slice(1));
447
- }
448
- function runSkillsCommand(args) {
449
- let workDir = process.cwd();
450
- for (let i = 0; i < args.length; i++) {
451
- switch (args[i]) {
452
- case "--workdir":
453
- case "-w":
454
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
455
- fail(`Missing value for ${args[i]}`);
456
- workDir = args[++i];
457
- break;
458
- case "--help":
459
- case "-h":
460
- printSkillsHelp();
461
- return;
462
- default:
463
- fail(`Unknown skills option: ${args[i]}`);
464
- }
465
- }
466
- const { skills, collisions } = discoverInstalledSkills(workDir);
467
- const output = skills.map((skill) => ({
468
- name: skill.name,
469
- description: skill.description,
470
- path: skill.baseDir,
471
- scope: resolveSkillScope(skill, workDir),
472
- }));
473
- process.stdout.write(`${JSON.stringify(output, null, 2)}\n`);
474
- for (const collision of collisions) {
475
- process.stderr.write(`[skill collision] "${collision.name}": kept ${collision.winnerPath}, ignored ${collision.loserPath}\n`);
476
- }
477
- }
478
- export async function runEnvCommand(args, io = {}) {
479
- const cwd = io.cwd ?? process.cwd();
480
- let envFilePath;
481
- let importEnvFilePath;
482
- let pasteKeys = false;
483
- for (let i = 0; i < args.length; i++) {
484
- switch (args[i]) {
485
- case "--env-file":
486
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
487
- fail(`Missing value for ${args[i]}`);
488
- envFilePath = args[++i];
489
- break;
490
- case "--import":
491
- case "-i":
492
- importEnvFilePath = args[i + 1]?.startsWith("-") ? "" : (args[++i] ?? "");
493
- break;
494
- case "--keys":
495
- pasteKeys = true;
496
- break;
497
- case "--help":
498
- case "-h":
499
- printEnvHelp();
500
- return;
501
- default:
502
- fail(`Unknown env option: ${args[i]}`);
503
- }
504
- }
505
- const targetEnvFile = envFilePath ? resolveUserPath(envFilePath, cwd) : defaultDuetEnvFilePath();
506
- const sourceEnvFile = importEnvFilePath === undefined
507
- ? undefined
508
- : importEnvFilePath
509
- ? resolveUserPath(importEnvFilePath, cwd)
510
- : join(cwd, ".env");
511
- const interactive = io.interactive ?? Boolean(process.stdin.isTTY && process.stderr.isTTY);
512
- if (sourceEnvFile === undefined && !pasteKeys) {
513
- (io.printHelp ?? printEnvHelp)();
514
- return;
515
- }
516
- if (sourceEnvFile !== undefined) {
517
- if (!(await fileExists(sourceEnvFile))) {
518
- fail(`No .env file found at ${sourceEnvFile}`);
519
- }
520
- await importEnvFile(sourceEnvFile, targetEnvFile);
521
- console.error(`Imported ${sourceEnvFile} into ${targetEnvFile}`);
522
- }
523
- if (pasteKeys) {
524
- if (!interactive) {
525
- fail("duet env --keys requires an interactive terminal");
526
- }
527
- const entries = await (io.promptForApiKeys ?? promptForApiKeys)();
528
- if (entries.size === 0) {
529
- console.error("No API keys entered.");
530
- return;
531
- }
532
- await mergeEnvEntries(targetEnvFile, entries);
533
- console.error(`Saved API keys to ${targetEnvFile}`);
534
- }
535
- }
536
- export async function runLoginCommand(args, io = {}) {
537
- const cwd = io.cwd ?? process.cwd();
538
- let envFilePathOverride = io.envFilePath;
539
- let noBrowser = false;
540
- let skipSkillSync = false;
541
- for (let i = 0; i < args.length; i++) {
542
- switch (args[i]) {
543
- case "--env-file":
544
- if (!args[i + 1] || args[i + 1]?.startsWith("-"))
545
- fail(`Missing value for ${args[i]}`);
546
- envFilePathOverride = args[++i];
547
- break;
548
- case "--no-browser":
549
- noBrowser = true;
550
- break;
551
- case "--skip-skill-sync":
552
- skipSkillSync = true;
553
- break;
554
- case "--help":
555
- case "-h":
556
- printLoginHelp();
557
- return;
558
- default:
559
- fail(`Unknown login option: ${args[i]}`);
560
- }
561
- }
562
- const targetEnvFile = envFilePathOverride
563
- ? resolveUserPath(envFilePathOverride, cwd)
564
- : defaultDuetEnvFilePath();
565
- const result = await loginWithBrowser({ noBrowser });
566
- await mergeEnvEntries(targetEnvFile, new Map([["DUET_API_KEY", result.apiKey]]));
567
- console.error(`Saved DUET_API_KEY for ${result.orgName} (${result.orgSlug}) to ${targetEnvFile}`);
568
- process.env.DUET_API_KEY = result.apiKey;
569
- shimDuetApiKeyToAiGateway();
570
- if (skipSkillSync) {
571
- console.error("Skipping default skill sync (--skip-skill-sync).");
572
- return;
573
- }
574
- console.error(`Checking default skills against ${resolveDuetAppBaseUrl()}...`);
575
- const syncResult = await syncDefaultSkills({ apiKey: result.apiKey });
576
- if (syncResult.status === "unchanged") {
577
- console.error("Default skills already up to date.");
578
- }
579
- else {
580
- console.error(`Synced ${syncResult.count} default skills.`);
581
- }
582
- }
583
- async function fileExists(path) {
584
- try {
585
- return (await stat(path)).isFile();
586
- }
587
- catch {
588
- return false;
589
- }
590
- }
591
- async function importEnvFile(source, target) {
592
- if (resolve(source) === resolve(target)) {
593
- console.error(`${target} is already the shared env file.`);
594
- return;
595
- }
596
- await mkdir(dirname(target), { recursive: true });
597
- if (!(await fileExists(target))) {
598
- await copyFile(source, target);
599
- return;
600
- }
601
- const parsed = dotenv.parse(await readFile(source));
602
- await mergeEnvEntries(target, new Map(Object.entries(parsed)));
603
- }
604
- async function promptForApiKeys() {
605
- const rl = createInterface({
606
- input: process.stdin,
607
- output: process.stderr,
608
- });
609
- try {
610
- const entries = new Map();
611
- console.error("Paste API keys for any providers you want to use. Leave blank to skip.");
612
- for (const key of SUPPORTED_API_KEYS) {
613
- const value = (await rl.question(`${key}: `)).trim();
614
- if (value)
615
- entries.set(key, value);
616
- }
617
- return entries;
618
- }
619
- finally {
620
- rl.close();
621
- }
622
- }
623
- async function mergeEnvEntries(target, entries) {
624
- await mkdir(dirname(target), { recursive: true });
625
- const existingText = (await fileExists(target)) ? await readFile(target, "utf8") : "";
626
- const merged = new Map(Object.entries(existingText ? dotenv.parse(existingText) : {}));
627
- for (const [key, value] of entries) {
628
- merged.set(key, value);
629
- }
630
- const text = formatEnvEntries(merged);
631
- await writeFile(target, text);
632
- }
633
- export function formatEnvEntries(entries) {
634
- return Array.from(entries, ([key, value]) => `${key}=${dotenvQuote(value)}`).join("\n") + "\n";
635
- }
636
- function dotenvQuote(value) {
637
- if (/^[A-Za-z0-9_./:@+-]+$/.test(value))
638
- return value;
639
- return JSON.stringify(value);
640
- }
641
- function parsePackageManager(value) {
642
- if (PACKAGE_MANAGERS.includes(value))
643
- return value;
644
- fail(`Unsupported package manager: ${value}`);
645
- }
646
- function detectPackageManager() {
647
- return detectPackageManagerFromContext({
648
- userAgent: process.env.npm_config_user_agent,
649
- runtimeExecutable: process.argv[0],
650
- cliFilePath: fileURLToPath(import.meta.url),
651
- scriptPath: process.argv[1],
652
- });
653
- }
654
- export function detectPackageManagerFromContext(context) {
655
- const userAgent = context.userAgent ?? "";
656
- for (const packageManager of PACKAGE_MANAGERS) {
657
- if (userAgent.startsWith(`${packageManager}/`))
658
- return packageManager;
659
- }
660
- for (const rawPath of [context.cliFilePath, context.scriptPath]) {
661
- const path = rawPath?.replace(/\\/g, "/");
662
- if (!path)
663
- continue;
664
- if (path.includes("/.bun/install/global/") || path.includes("/.bun/bin/"))
665
- return "bun";
666
- if (path.includes("/.pnpm/") || path.includes("/share/pnpm/"))
667
- return "pnpm";
668
- if (path.includes("/.config/yarn/global/") || path.includes("/yarn/global/"))
669
- return "yarn";
670
- if (path.includes("/node_modules/"))
671
- return "npm";
672
- }
673
- if (basename(context.runtimeExecutable ?? "").includes("bun"))
674
- return "bun";
675
- return "npm";
676
- }
677
- export function globalUpgradeCommand(packageManager, packageName, version) {
678
- const packageSpec = `${packageName}@${normalizePackageVersion(version)}`;
679
- if (packageManager === "bun")
680
- return ["bun", "add", "--global", packageSpec];
681
- if (packageManager === "pnpm")
682
- return ["pnpm", "add", "--global", packageSpec];
683
- if (packageManager === "yarn")
684
- return ["yarn", "global", "add", packageSpec];
685
- return ["npm", "install", "--global", packageSpec];
686
- }
687
- function normalizePackageVersion(version) {
688
- return version.replace(/^v/, "");
689
- }
690
- async function runCommand(command, args) {
691
- const child = spawn(command, args, { stdio: "inherit" });
692
- const exitCode = await new Promise((resolve, reject) => {
693
- child.once("error", reject);
694
- child.once("exit", resolve);
695
- });
696
- if (exitCode !== 0) {
697
- process.exitCode = exitCode ?? 1;
698
- }
699
- }
700
- async function readInteractivePrompt() {
701
- const rl = createInterface({
702
- input: process.stdin,
703
- output: process.stderr,
704
- });
705
- try {
706
- let prompt = "";
707
- while (!prompt) {
708
- prompt = (await rl.question("> ")).trim();
709
- }
710
- return prompt;
711
- }
712
- finally {
713
- rl.close();
714
- }
715
- }
716
- export function resumeCommand(sessionId, input) {
717
- const command = [
718
- detectInvocationPrefix(),
719
- "--resume",
720
- shellQuote(sessionId),
721
- "--workdir",
722
- shellQuote(input.workDir),
723
- ];
724
- if (input.modelName) {
725
- command.push("--model", shellQuote(input.modelName));
726
- }
727
- if (input.memoryModelName) {
728
- command.push("--memory-model", shellQuote(input.memoryModelName));
729
- }
730
- if (input.disableDurableMemory) {
731
- command.push("--no-memory");
732
- }
733
- if (input.systemInstructions) {
734
- command.push("--system-prompt", shellQuote(input.systemInstructions));
735
- }
736
- if (input.systemPromptFiles) {
737
- if (input.systemPromptFiles.length === 0) {
738
- command.push("--no-system-prompt-files");
739
- }
740
- else {
741
- for (const fileName of input.systemPromptFiles) {
742
- command.push("--system-prompt-file", shellQuote(fileName));
743
- }
744
- }
745
- }
746
- if (input.envFilePath) {
747
- command.push("--env-file", shellQuote(input.envFilePath));
748
- }
749
- if (input.resumeHistoryLines !== undefined) {
750
- command.push("--resume-history-lines", String(input.resumeHistoryLines));
751
- }
752
- return command.join(" ");
753
- }
754
- // Detect how this CLI was invoked so the resume hint copy-pastes back into
755
- // the user's actual shell. `bun run cli` and `bun src/cli.ts` are common
756
- // during local development; the published bin is `duet`.
757
- function detectInvocationPrefix() {
758
- const scriptPath = process.argv[1] ?? "";
759
- const base = basename(scriptPath);
760
- if (process.env.npm_lifecycle_event === "cli")
761
- return "bun run cli";
762
- if (base === "cli.ts" || scriptPath.includes("/src/cli.ts"))
763
- return "bun src/cli.ts";
764
- return "duet";
765
- }
766
- function shellQuote(value) {
767
- if (/^[A-Za-z0-9_./:=+-]+$/.test(value))
768
- return value;
769
- return `'${value.replace(/'/g, "'\\''")}'`;
770
- }
771
- function printHelp(packageName) {
772
- console.log(`
773
- duet — An opinionated full-stack agent runner
774
-
775
- USAGE
776
- duet [options] [prompt]
777
- duet login [--no-browser] [--skip-skill-sync]
778
- duet env [--env-file <path>] [--import [path]|--keys]
779
- duet skills [--workdir <path>]
780
- duet upgrade [--manager npm|bun|pnpm|yarn]
781
- echo "prompt" | duet
782
-
783
- COMMANDS
784
- login Sign in via browser; saves DUET_API_KEY and syncs default skills (recommended)
785
- env Manually create or update the shared duet env file with provider API keys
786
- skills List installed skills as JSON (name, description, path, scope)
787
- upgrade Upgrade the global ${packageName} installation
788
-
789
- OPTIONS
790
- -m, --model <name> TurnRunner model override
791
- --memory-model <name> Observational memory model (default inferred from provider env)
792
- --no-memory Keep memory in-process; do not read or write durable memory
793
- -w, --workdir <path> Working directory (default: cwd)
794
- -r, --resume <id> Resume a saved session
795
- --resume-history-lines <n>
796
- Display up to n prior-session lines in the TUI (default: ${DEFAULT_RESUME_HISTORY_LINES})
797
- --system-prompt <text> Additional system instructions for the runner
798
- --system-prompt-file <path>
799
- Load a file into the system prompt; repeatable
800
- --no-system-prompt-files Disable default AGENTS.md system prompt loading
801
- --env-file <path> Shared env file to load after <workdir>/.env (default: ${DEFAULT_DUET_ENV_FILE})
802
- --json Force JSONL event output instead of the TUI
803
- -v, --version Print the installed duet version and exit
804
- -h, --help Show this help
805
-
806
- INTERACTIVE
807
- In a TTY, duet keeps one local session open after terminal events.
808
- Type /exit or /quit to end the conversation.
809
-
810
- MODELS
811
- Prefer shorthands like opus-4.7, sonnet-4.6, haiku-4.5, and gpt-5.5.
812
- They map to the first configured provider that supports that model.
813
- Full provider:modelId syntax is also supported, e.g. anthropic:claude-opus-4-7.
814
- If omitted, duet infers a default from ANTHROPIC_API_KEY,
815
- DUET_API_KEY, AI_GATEWAY_API_KEY, OPENROUTER_API_KEY, or
816
- OPENAI_API_KEY after loading <workdir>/.env and the shared duet env file.
817
-
818
- duet-gateway: routes through the Duet gateway proxy
819
- (https://duet.so/api/v1/ai-gateway by default; override the app origin
820
- via DUET_APP_BASE_URL). It mirrors vercel-ai-gateway's model catalog
821
- and authenticates with DUET_API_KEY.
822
-
823
- EXAMPLES
824
- duet "build a REST API with Express and TypeScript"
825
- duet -m gpt-5.5 "analyze the performance of our test suite"
826
- duet --memory-model sonnet-4.6 "summarize this repo"
827
- duet -m opus-4.7 "refactor the auth module"
828
- duet --system-prompt "Prefer concise answers." "review this repo"
829
- duet --system-prompt-file TEAM.md "review this repo"
830
- duet --env-file ~/.config/duet/env "review this repo"
831
- duet --workdir ./my-project "refactor the auth module"
832
- duet --resume session_abc123 --workdir ./my-project
833
- duet login
834
- duet env
835
- duet upgrade
836
- `);
837
- }
838
- function printLoginHelp() {
839
- console.log(`
840
- duet login — Sign in via browser and sync default skills
841
-
842
- USAGE
843
- duet login [--env-file <path>] [--no-browser] [--skip-skill-sync]
844
-
845
- Opens a browser window pointed at the Duet web app, waits for the user to
846
- confirm, then writes the org's DUET_API_KEY to the shared env file. After
847
- auth, fetches and writes the latest default skills to ~/.duet/skills.
848
-
849
- OPTIONS
850
- --env-file <path> Env file to write the API key to (default: ${DEFAULT_DUET_ENV_FILE})
851
- --no-browser Print the auth URL instead of opening a browser
852
- --skip-skill-sync Skip the post-login default skills sync
853
- -h, --help Show this help
854
-
855
- SKILL SYNC
856
- Mirrors the sandbox protocol: hashes the rendered skill payload and only
857
- rewrites ~/.duet/skills when the hash differs from ~/.duet/.skills-hash.
858
-
859
- OVERRIDES
860
- Set DUET_APP_BASE_URL (e.g. https://staging.duet.so) to re-point both the
861
- AI gateway provider and the CLI auth/sync endpoints at a non-production
862
- deployment.
863
- `);
864
- }
865
- function printEnvHelp() {
866
- console.log(`
867
- duet env — Create or update a shared duet env file
868
-
869
- USAGE
870
- duet env [--env-file <path>] [--import [path]|--keys]
871
-
872
- Prefer \`duet login\` for the standard setup flow. Use \`duet env\` when you
873
- want manual control over which provider API keys land in the shared env file.
874
-
875
- By default, env only prints this help. Choose --import to copy
876
- provider keys from cwd .env or a provided env file, or --keys to paste keys interactively.
877
-
878
- OPTIONS
879
- --env-file <path> Env file to write (default: ${DEFAULT_DUET_ENV_FILE})
880
- -i, --import [path] Import cwd .env, or import the provided env file
881
- --keys Prompt for supported provider API keys
882
- -h, --help Show this help
883
-
884
- SUPPORTED KEYS
885
- ${SUPPORTED_API_KEYS.join(", ")}
886
- `);
887
- }
888
- function printSkillsHelp() {
889
- console.log(`
890
- duet skills — List installed skills as JSON
891
-
892
- USAGE
893
- duet skills [--workdir <path>]
894
-
895
- OPTIONS
896
- -w, --workdir <path> Working directory for project-local skills (default: cwd)
897
- -h, --help Show this help
898
-
899
- OUTPUT
900
- Prints a JSON array of installed skills. Each entry has:
901
- name Skill name
902
- description Skill description (from frontmatter, raw — no shell expansion)
903
- path Absolute path to the skill directory
904
- scope "user", "project", or "temporary"
905
- `);
906
- }
907
- function printUpgradeHelp(packageName) {
908
- console.log(`
909
- duet upgrade — Upgrade the global ${packageName} installation
910
-
911
- USAGE
912
- duet upgrade [--manager npm|bun|pnpm|yarn] [--version <version>]
913
-
914
- OPTIONS
915
- --manager <name> Package manager to use (default: detected, fallback: npm)
916
- --version <version> Install an exact version instead of npm's latest dist-tag
917
- --dry-run Print the upgrade command without running it
918
- -h, --help Show this help
919
- `);
920
71
  }
921
72
  if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
922
- main();
73
+ void main();
923
74
  }
924
75
  //# sourceMappingURL=cli.js.map