@duetso/agent 0.1.33 → 0.1.35
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 +2 -0
- package/dist/package.json +1 -1
- package/dist/src/cli/env.d.ts +18 -0
- package/dist/src/cli/env.d.ts.map +1 -0
- package/dist/src/cli/env.js +114 -0
- package/dist/src/cli/env.js.map +1 -0
- package/dist/src/cli/help.d.ts +8 -0
- package/dist/src/cli/help.d.ts.map +1 -0
- package/dist/src/cli/help.js +175 -0
- package/dist/src/cli/help.js.map +1 -0
- package/dist/src/cli/login.d.ts +13 -0
- package/dist/src/cli/login.d.ts.map +1 -0
- package/dist/src/cli/login.js +61 -0
- package/dist/src/cli/login.js.map +1 -0
- package/dist/src/cli/memories-db.d.ts +24 -0
- package/dist/src/cli/memories-db.d.ts.map +1 -0
- package/dist/src/cli/memories-db.js +74 -0
- package/dist/src/cli/memories-db.js.map +1 -0
- package/dist/src/cli/memories-tui.d.ts +11 -0
- package/dist/src/cli/memories-tui.d.ts.map +1 -0
- package/dist/src/cli/memories-tui.js +266 -0
- package/dist/src/cli/memories-tui.js.map +1 -0
- package/dist/src/cli/memories.d.ts +9 -0
- package/dist/src/cli/memories.d.ts.map +1 -0
- package/dist/src/cli/memories.js +38 -0
- package/dist/src/cli/memories.js.map +1 -0
- package/dist/src/cli/package-manager.d.ts +38 -0
- package/dist/src/cli/package-manager.d.ts.map +1 -0
- package/dist/src/cli/package-manager.js +78 -0
- package/dist/src/cli/package-manager.js.map +1 -0
- package/dist/src/cli/resume-hint.d.ts +22 -0
- package/dist/src/cli/resume-hint.d.ts.map +1 -0
- package/dist/src/cli/resume-hint.js +61 -0
- package/dist/src/cli/resume-hint.js.map +1 -0
- package/dist/src/cli/run.d.ts +43 -0
- package/dist/src/cli/run.d.ts.map +1 -0
- package/dist/src/cli/run.js +273 -0
- package/dist/src/cli/run.js.map +1 -0
- package/dist/src/cli/shared.d.ts +55 -0
- package/dist/src/cli/shared.d.ts.map +1 -0
- package/dist/src/cli/shared.js +125 -0
- package/dist/src/cli/shared.js.map +1 -0
- package/dist/src/cli/skills.d.ts +10 -0
- package/dist/src/cli/skills.d.ts.map +1 -0
- package/dist/src/cli/skills.js +42 -0
- package/dist/src/cli/skills.js.map +1 -0
- package/dist/src/cli/upgrade.d.ts +9 -0
- package/dist/src/cli/upgrade.d.ts.map +1 -0
- package/dist/src/cli/upgrade.js +52 -0
- package/dist/src/cli/upgrade.js.map +1 -0
- package/dist/src/cli/version-check.d.ts +22 -0
- package/dist/src/cli/version-check.d.ts.map +1 -0
- package/dist/src/cli/version-check.js +78 -0
- package/dist/src/cli/version-check.js.map +1 -0
- package/dist/src/cli.d.ts +20 -63
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +38 -887
- package/dist/src/cli.js.map +1 -1
- package/dist/src/memory/observational-prompts.d.ts.map +1 -1
- package/dist/src/memory/observational-prompts.js +11 -7
- package/dist/src/memory/observational-prompts.js.map +1 -1
- package/dist/src/tui/app.d.ts +7 -47
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +279 -396
- package/dist/src/tui/app.js.map +1 -1
- package/dist/src/tui/autocomplete.d.ts +91 -0
- package/dist/src/tui/autocomplete.d.ts.map +1 -0
- package/dist/src/tui/autocomplete.js +177 -0
- package/dist/src/tui/autocomplete.js.map +1 -0
- package/dist/src/tui/file-index.d.ts +11 -0
- package/dist/src/tui/file-index.d.ts.map +1 -0
- package/dist/src/tui/file-index.js +75 -0
- package/dist/src/tui/file-index.js.map +1 -0
- package/dist/src/tui/history.d.ts +50 -0
- package/dist/src/tui/history.d.ts.map +1 -0
- package/dist/src/tui/history.js +132 -0
- package/dist/src/tui/history.js.map +1 -0
- package/dist/src/tui/sidebar.d.ts +20 -0
- package/dist/src/tui/sidebar.d.ts.map +1 -0
- package/dist/src/tui/sidebar.js +118 -0
- package/dist/src/tui/sidebar.js.map +1 -0
- package/dist/src/tui/theme.d.ts +15 -0
- package/dist/src/tui/theme.d.ts.map +1 -0
- package/dist/src/tui/theme.js +18 -0
- package/dist/src/tui/theme.js.map +1 -0
- package/dist/src/turn-runner/prompts.d.ts.map +1 -1
- package/dist/src/turn-runner/prompts.js +7 -0
- package/dist/src/turn-runner/prompts.js.map +1 -1
- package/dist/src/turn-runner/tools.d.ts +15 -1
- package/dist/src/turn-runner/tools.d.ts.map +1 -1
- package/dist/src/turn-runner/tools.js +42 -9
- package/dist/src/turn-runner/tools.js.map +1 -1
- package/package.json +1 -1
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 {
|
|
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 { runMemoriesCommand } from "./cli/memories.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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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 { runMemoriesCommand } from "./cli/memories.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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
56
|
-
}
|
|
57
|
-
if (args[0] === "skills") {
|
|
58
|
-
try {
|
|
49
|
+
if (subcommand === "skills") {
|
|
59
50
|
runSkillsCommand(args.slice(1));
|
|
51
|
+
return;
|
|
60
52
|
}
|
|
61
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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 === "memories") {
|
|
62
|
+
await runMemoriesCommand(args.slice(1));
|
|
63
|
+
return;
|
|
275
64
|
}
|
|
276
|
-
|
|
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
|