@otto-code/cli 0.5.0
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/bin/otto +3 -0
- package/dist/classify.d.ts +19 -0
- package/dist/classify.js +49 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.js +113 -0
- package/dist/commands/agent/archive.d.ts +18 -0
- package/dist/commands/agent/archive.js +102 -0
- package/dist/commands/agent/attach.d.ts +11 -0
- package/dist/commands/agent/attach.js +159 -0
- package/dist/commands/agent/delete.d.ts +15 -0
- package/dist/commands/agent/delete.js +107 -0
- package/dist/commands/agent/import.d.ts +14 -0
- package/dist/commands/agent/import.js +122 -0
- package/dist/commands/agent/index.d.ts +3 -0
- package/dist/commands/agent/index.js +48 -0
- package/dist/commands/agent/inspect.d.ts +15 -0
- package/dist/commands/agent/inspect.js +209 -0
- package/dist/commands/agent/logs.d.ts +19 -0
- package/dist/commands/agent/logs.js +180 -0
- package/dist/commands/agent/ls.d.ts +44 -0
- package/dist/commands/agent/ls.js +204 -0
- package/dist/commands/agent/mode.d.ts +18 -0
- package/dist/commands/agent/mode.js +100 -0
- package/dist/commands/agent/reload.d.ts +15 -0
- package/dist/commands/agent/reload.js +75 -0
- package/dist/commands/agent/run.d.ts +44 -0
- package/dist/commands/agent/run.js +464 -0
- package/dist/commands/agent/send.d.ts +19 -0
- package/dist/commands/agent/send.js +187 -0
- package/dist/commands/agent/stop.d.ts +17 -0
- package/dist/commands/agent/stop.js +115 -0
- package/dist/commands/agent/update.d.ts +18 -0
- package/dist/commands/agent/update.js +139 -0
- package/dist/commands/agent/wait.d.ts +17 -0
- package/dist/commands/agent/wait.js +158 -0
- package/dist/commands/chat/create.d.ts +9 -0
- package/dist/commands/chat/create.js +23 -0
- package/dist/commands/chat/delete.d.ts +6 -0
- package/dist/commands/chat/delete.js +20 -0
- package/dist/commands/chat/index.d.ts +3 -0
- package/dist/commands/chat/index.js +47 -0
- package/dist/commands/chat/inspect.d.ts +6 -0
- package/dist/commands/chat/inspect.js +20 -0
- package/dist/commands/chat/ls.d.ts +6 -0
- package/dist/commands/chat/ls.js +20 -0
- package/dist/commands/chat/post.d.ts +9 -0
- package/dist/commands/chat/post.js +28 -0
- package/dist/commands/chat/read.d.ts +11 -0
- package/dist/commands/chat/read.js +40 -0
- package/dist/commands/chat/schema.d.ts +36 -0
- package/dist/commands/chat/schema.js +81 -0
- package/dist/commands/chat/shared.d.ts +19 -0
- package/dist/commands/chat/shared.js +118 -0
- package/dist/commands/chat/wait.d.ts +9 -0
- package/dist/commands/chat/wait.js +45 -0
- package/dist/commands/daemon/index.d.ts +3 -0
- package/dist/commands/daemon/index.js +57 -0
- package/dist/commands/daemon/local-daemon.d.ts +78 -0
- package/dist/commands/daemon/local-daemon.js +544 -0
- package/dist/commands/daemon/pair.d.ts +9 -0
- package/dist/commands/daemon/pair.js +76 -0
- package/dist/commands/daemon/restart.d.ts +12 -0
- package/dist/commands/daemon/restart.js +99 -0
- package/dist/commands/daemon/runtime-toolchain.d.ts +6 -0
- package/dist/commands/daemon/runtime-toolchain.js +87 -0
- package/dist/commands/daemon/set-password.d.ts +17 -0
- package/dist/commands/daemon/set-password.js +82 -0
- package/dist/commands/daemon/start.d.ts +6 -0
- package/dist/commands/daemon/start.js +56 -0
- package/dist/commands/daemon/status.d.ts +10 -0
- package/dist/commands/daemon/status.js +338 -0
- package/dist/commands/daemon/stop.d.ts +15 -0
- package/dist/commands/daemon/stop.js +60 -0
- package/dist/commands/hooks.d.ts +19 -0
- package/dist/commands/hooks.js +89 -0
- package/dist/commands/loop/index.d.ts +3 -0
- package/dist/commands/loop/index.js +18 -0
- package/dist/commands/loop/inspect.d.ts +13 -0
- package/dist/commands/loop/inspect.js +100 -0
- package/dist/commands/loop/logs.d.ts +8 -0
- package/dist/commands/loop/logs.js +69 -0
- package/dist/commands/loop/ls.d.ts +18 -0
- package/dist/commands/loop/ls.js +62 -0
- package/dist/commands/loop/run.d.ts +30 -0
- package/dist/commands/loop/run.js +162 -0
- package/dist/commands/loop/stop.d.ts +15 -0
- package/dist/commands/loop/stop.js +56 -0
- package/dist/commands/loop/types.d.ts +138 -0
- package/dist/commands/loop/types.js +2 -0
- package/dist/commands/onboard.d.ts +10 -0
- package/dist/commands/onboard.js +416 -0
- package/dist/commands/open.d.ts +2 -0
- package/dist/commands/open.js +81 -0
- package/dist/commands/permit/allow.d.ts +20 -0
- package/dist/commands/permit/allow.js +133 -0
- package/dist/commands/permit/deny.d.ts +12 -0
- package/dist/commands/permit/deny.js +105 -0
- package/dist/commands/permit/index.d.ts +3 -0
- package/dist/commands/permit/index.js +27 -0
- package/dist/commands/permit/ls.d.ts +18 -0
- package/dist/commands/permit/ls.js +66 -0
- package/dist/commands/provider/index.d.ts +3 -0
- package/dist/commands/provider/index.js +16 -0
- package/dist/commands/provider/ls.d.ts +19 -0
- package/dist/commands/provider/ls.js +73 -0
- package/dist/commands/provider/models.d.ts +20 -0
- package/dist/commands/provider/models.js +53 -0
- package/dist/commands/schedule/create.d.ts +18 -0
- package/dist/commands/schedule/create.js +40 -0
- package/dist/commands/schedule/delete.d.ts +10 -0
- package/dist/commands/schedule/delete.js +32 -0
- package/dist/commands/schedule/index.d.ts +3 -0
- package/dist/commands/schedule/index.js +66 -0
- package/dist/commands/schedule/inspect.d.ts +6 -0
- package/dist/commands/schedule/inspect.js +24 -0
- package/dist/commands/schedule/logs.d.ts +6 -0
- package/dist/commands/schedule/logs.js +23 -0
- package/dist/commands/schedule/ls.d.ts +5 -0
- package/dist/commands/schedule/ls.js +23 -0
- package/dist/commands/schedule/pause.d.ts +5 -0
- package/dist/commands/schedule/pause.js +23 -0
- package/dist/commands/schedule/resume.d.ts +5 -0
- package/dist/commands/schedule/resume.js +23 -0
- package/dist/commands/schedule/run-once.d.ts +5 -0
- package/dist/commands/schedule/run-once.js +23 -0
- package/dist/commands/schedule/schema.d.ts +21 -0
- package/dist/commands/schedule/schema.js +67 -0
- package/dist/commands/schedule/shared.d.ts +56 -0
- package/dist/commands/schedule/shared.js +346 -0
- package/dist/commands/schedule/types.d.ts +168 -0
- package/dist/commands/schedule/types.js +2 -0
- package/dist/commands/schedule/update.d.ts +21 -0
- package/dist/commands/schedule/update.js +39 -0
- package/dist/commands/speech/index.d.ts +3 -0
- package/dist/commands/speech/index.js +5 -0
- package/dist/commands/terminal/capture.d.ts +10 -0
- package/dist/commands/terminal/capture.js +68 -0
- package/dist/commands/terminal/create.d.ts +10 -0
- package/dist/commands/terminal/create.js +38 -0
- package/dist/commands/terminal/index.d.ts +3 -0
- package/dist/commands/terminal/index.js +43 -0
- package/dist/commands/terminal/kill.d.ts +6 -0
- package/dist/commands/terminal/kill.js +36 -0
- package/dist/commands/terminal/ls.d.ts +10 -0
- package/dist/commands/terminal/ls.js +21 -0
- package/dist/commands/terminal/schema.d.ts +18 -0
- package/dist/commands/terminal/schema.js +23 -0
- package/dist/commands/terminal/send-keys.d.ts +7 -0
- package/dist/commands/terminal/send-keys.js +76 -0
- package/dist/commands/terminal/shared.d.ts +12 -0
- package/dist/commands/terminal/shared.js +61 -0
- package/dist/commands/worktree/archive.d.ts +20 -0
- package/dist/commands/worktree/archive.js +104 -0
- package/dist/commands/worktree/create-input.d.ts +15 -0
- package/dist/commands/worktree/create-input.js +56 -0
- package/dist/commands/worktree/create.d.ts +11 -0
- package/dist/commands/worktree/create.js +58 -0
- package/dist/commands/worktree/index.d.ts +3 -0
- package/dist/commands/worktree/index.js +25 -0
- package/dist/commands/worktree/ls.d.ts +19 -0
- package/dist/commands/worktree/ls.js +97 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/output/index.d.ts +45 -0
- package/dist/output/index.js +47 -0
- package/dist/output/json.d.ts +11 -0
- package/dist/output/json.js +37 -0
- package/dist/output/quiet.d.ts +9 -0
- package/dist/output/quiet.js +22 -0
- package/dist/output/render.d.ts +15 -0
- package/dist/output/render.js +83 -0
- package/dist/output/table.d.ts +13 -0
- package/dist/output/table.js +135 -0
- package/dist/output/types.d.ts +73 -0
- package/dist/output/types.js +8 -0
- package/dist/output/with-output.d.ts +41 -0
- package/dist/output/with-output.js +87 -0
- package/dist/output/yaml.d.ts +11 -0
- package/dist/output/yaml.js +38 -0
- package/dist/run.d.ts +14 -0
- package/dist/run.js +33 -0
- package/dist/utils/client-id.d.ts +2 -0
- package/dist/utils/client-id.js +37 -0
- package/dist/utils/client.d.ts +44 -0
- package/dist/utils/client.js +330 -0
- package/dist/utils/command-options.d.ts +6 -0
- package/dist/utils/command-options.js +17 -0
- package/dist/utils/duration.d.ts +7 -0
- package/dist/utils/duration.js +38 -0
- package/dist/utils/errors.d.ts +5 -0
- package/dist/utils/errors.js +10 -0
- package/dist/utils/paths.d.ts +12 -0
- package/dist/utils/paths.js +22 -0
- package/dist/utils/provider-model.d.ts +11 -0
- package/dist/utils/provider-model.js +49 -0
- package/dist/utils/timeline.d.ts +11 -0
- package/dist/utils/timeline.js +11 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +10 -0
- package/package.json +48 -0
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import { cancel, confirm, intro, isCancel, log, note, outro, spinner } from "@clack/prompts";
|
|
2
|
+
import { Command, Option } from "commander";
|
|
3
|
+
import { writeFileSync } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { generateLocalPairingOffer, loadConfig, loadPersistedConfig, } from "@otto-code/server";
|
|
6
|
+
import { resolveLocalOttoHome, resolveLocalDaemonState, resolveTcpHostFromListen, startLocalDaemonDetached, tailDaemonLog, } from "./daemon/local-daemon.js";
|
|
7
|
+
import { tryConnectToDaemon } from "../utils/client.js";
|
|
8
|
+
const DEFAULT_READY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
9
|
+
const READY_PROBE_TIMEOUT_MS = 1200;
|
|
10
|
+
class OnboardCancelledError extends Error {
|
|
11
|
+
}
|
|
12
|
+
const plainNoteFormat = (line) => line;
|
|
13
|
+
function renderNote(message, title) {
|
|
14
|
+
note(message, title, { format: plainNoteFormat });
|
|
15
|
+
}
|
|
16
|
+
function sleep(ms) {
|
|
17
|
+
return new Promise((resolve) => {
|
|
18
|
+
setTimeout(resolve, ms);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function parseTimeoutMs(raw) {
|
|
22
|
+
if (!raw || raw.trim().length === 0) {
|
|
23
|
+
return DEFAULT_READY_TIMEOUT_MS;
|
|
24
|
+
}
|
|
25
|
+
const seconds = Number(raw);
|
|
26
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
27
|
+
throw new Error(`Invalid timeout value: ${raw}`);
|
|
28
|
+
}
|
|
29
|
+
return Math.ceil(seconds * 1000);
|
|
30
|
+
}
|
|
31
|
+
function toCliOverrides(options) {
|
|
32
|
+
const cliOverrides = {};
|
|
33
|
+
if (options.listen) {
|
|
34
|
+
cliOverrides.listen = options.listen;
|
|
35
|
+
}
|
|
36
|
+
else if (options.port) {
|
|
37
|
+
cliOverrides.listen = `127.0.0.1:${options.port}`;
|
|
38
|
+
}
|
|
39
|
+
if (options.relay === false) {
|
|
40
|
+
cliOverrides.relayEnabled = false;
|
|
41
|
+
}
|
|
42
|
+
if (options.hostnames) {
|
|
43
|
+
const raw = options.hostnames.trim();
|
|
44
|
+
cliOverrides.hostnames =
|
|
45
|
+
raw.toLowerCase() === "true"
|
|
46
|
+
? true
|
|
47
|
+
: raw
|
|
48
|
+
.split(",")
|
|
49
|
+
.map((host) => host.trim())
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
if (options.mcp === false) {
|
|
53
|
+
cliOverrides.mcpEnabled = false;
|
|
54
|
+
}
|
|
55
|
+
return cliOverrides;
|
|
56
|
+
}
|
|
57
|
+
function savePersistedConfig(ottoHome, config) {
|
|
58
|
+
const configPath = path.join(ottoHome, "config.json");
|
|
59
|
+
writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
60
|
+
}
|
|
61
|
+
function applyVoiceSelection(config, enabled) {
|
|
62
|
+
return {
|
|
63
|
+
...config,
|
|
64
|
+
features: {
|
|
65
|
+
...config.features,
|
|
66
|
+
dictation: {
|
|
67
|
+
...config.features?.dictation,
|
|
68
|
+
enabled,
|
|
69
|
+
},
|
|
70
|
+
voiceMode: {
|
|
71
|
+
...config.features?.voiceMode,
|
|
72
|
+
enabled,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function resolvePersistedVoiceSelection(config) {
|
|
78
|
+
const voiceModeEnabled = config.features?.voiceMode?.enabled;
|
|
79
|
+
if (typeof voiceModeEnabled === "boolean") {
|
|
80
|
+
return voiceModeEnabled;
|
|
81
|
+
}
|
|
82
|
+
const dictationEnabled = config.features?.dictation?.enabled;
|
|
83
|
+
if (typeof dictationEnabled === "boolean") {
|
|
84
|
+
return dictationEnabled;
|
|
85
|
+
}
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
async function resolveVoiceSelection(mode) {
|
|
89
|
+
if (mode === "enable") {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
if (mode === "disable") {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
96
|
+
log.message("Non-interactive terminal detected; voice setup defaults to disabled.");
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const answer = await confirm({
|
|
100
|
+
message: "Enable voice features? (downloads local STT/TTS models in background)",
|
|
101
|
+
active: "Yes",
|
|
102
|
+
inactive: "No",
|
|
103
|
+
initialValue: false,
|
|
104
|
+
});
|
|
105
|
+
if (isCancel(answer)) {
|
|
106
|
+
throw new OnboardCancelledError("Onboarding cancelled by user.");
|
|
107
|
+
}
|
|
108
|
+
return answer;
|
|
109
|
+
}
|
|
110
|
+
function parseDownloadProgress(logTail) {
|
|
111
|
+
const lines = logTail.split("\n").filter(Boolean);
|
|
112
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
113
|
+
const line = lines[index];
|
|
114
|
+
if (!line || !line.includes("Downloading model artifact")) {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const pctMatch = line.match(/"pct"\s*:\s*(\d{1,3})|\bpct[=:]\s*(\d{1,3})/);
|
|
118
|
+
const modelMatch = line.match(/"modelId"\s*:\s*"([^"]+)"|\bmodelId[=:]\s*"?([^\s",}]+)/);
|
|
119
|
+
return {
|
|
120
|
+
modelId: modelMatch?.[1] ?? modelMatch?.[2] ?? null,
|
|
121
|
+
pct: pctMatch ? Number(pctMatch[1] ?? pctMatch[2]) : null,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
function renderProgressLine(progress) {
|
|
127
|
+
const modelSuffix = progress.modelId ? ` (${progress.modelId})` : "";
|
|
128
|
+
if (progress.pct === null) {
|
|
129
|
+
return `Downloading speech model${modelSuffix}...`;
|
|
130
|
+
}
|
|
131
|
+
return `Downloading speech model${modelSuffix}: ${progress.pct}%`;
|
|
132
|
+
}
|
|
133
|
+
async function probeDaemonReady(home, timeoutMs) {
|
|
134
|
+
const state = resolveLocalDaemonState({ home });
|
|
135
|
+
const host = resolveTcpHostFromListen(state.listen);
|
|
136
|
+
const deadline = Date.now() + timeoutMs;
|
|
137
|
+
const remainingTimeoutMs = () => Math.max(1, deadline - Date.now());
|
|
138
|
+
if (state.running && host) {
|
|
139
|
+
const client = await tryConnectToDaemon({
|
|
140
|
+
host,
|
|
141
|
+
timeout: Math.min(remainingTimeoutMs(), READY_PROBE_TIMEOUT_MS),
|
|
142
|
+
});
|
|
143
|
+
if (client) {
|
|
144
|
+
try {
|
|
145
|
+
await client.fetchAgents({
|
|
146
|
+
timeout: Math.min(remainingTimeoutMs(), READY_PROBE_TIMEOUT_MS),
|
|
147
|
+
});
|
|
148
|
+
return { kind: "ready", listen: state.listen, host };
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Daemon process is alive but not API-ready yet.
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
await client.close().catch(() => { });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (state.running && !host) {
|
|
159
|
+
return { kind: "ready", listen: state.listen, host: null };
|
|
160
|
+
}
|
|
161
|
+
return { kind: "pending" };
|
|
162
|
+
}
|
|
163
|
+
function announceProgress(home, state, onStatus) {
|
|
164
|
+
const progress = parseDownloadProgress(tailDaemonLog(home, 120) ?? "");
|
|
165
|
+
const progressLine = progress ? renderProgressLine(progress) : null;
|
|
166
|
+
const statusMessage = progressLine ?? "Waiting for daemon to become ready...";
|
|
167
|
+
if (statusMessage !== state.lastStatus) {
|
|
168
|
+
onStatus?.(statusMessage);
|
|
169
|
+
return { lastStatus: statusMessage, lastPrintedAt: Date.now() };
|
|
170
|
+
}
|
|
171
|
+
if (!onStatus && Date.now() - state.lastPrintedAt >= 3000) {
|
|
172
|
+
console.log(statusMessage);
|
|
173
|
+
return { lastStatus: state.lastStatus, lastPrintedAt: Date.now() };
|
|
174
|
+
}
|
|
175
|
+
return state;
|
|
176
|
+
}
|
|
177
|
+
async function waitForDaemonReady(args) {
|
|
178
|
+
const deadline = Date.now() + args.timeoutMs;
|
|
179
|
+
const createTimeoutError = () => {
|
|
180
|
+
const recentLogs = tailDaemonLog(args.home, 60);
|
|
181
|
+
return new Error([
|
|
182
|
+
`Timed out after ${Math.ceil(args.timeoutMs / 1000)}s waiting for daemon readiness.`,
|
|
183
|
+
recentLogs ? `Recent daemon logs:\n${recentLogs}` : null,
|
|
184
|
+
]
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join("\n\n"));
|
|
187
|
+
};
|
|
188
|
+
async function poll(state) {
|
|
189
|
+
if (Date.now() >= deadline) {
|
|
190
|
+
throw createTimeoutError();
|
|
191
|
+
}
|
|
192
|
+
const probe = await probeDaemonReady(args.home, Math.max(1, deadline - Date.now()));
|
|
193
|
+
if (probe.kind === "ready") {
|
|
194
|
+
return { listen: probe.listen, host: probe.host };
|
|
195
|
+
}
|
|
196
|
+
const nextState = announceProgress(args.home, state, args.onStatus);
|
|
197
|
+
if (Date.now() >= deadline) {
|
|
198
|
+
throw createTimeoutError();
|
|
199
|
+
}
|
|
200
|
+
await sleep(200);
|
|
201
|
+
return poll(nextState);
|
|
202
|
+
}
|
|
203
|
+
return poll({ lastStatus: "", lastPrintedAt: 0 });
|
|
204
|
+
}
|
|
205
|
+
function printNextSteps(pairingUrl, ottoHome, richUi) {
|
|
206
|
+
const daemonLogPath = path.join(ottoHome, "daemon.log");
|
|
207
|
+
const nextStepsLines = [
|
|
208
|
+
pairingUrl
|
|
209
|
+
? "1. Open Otto and scan the QR code above, or paste the pairing link."
|
|
210
|
+
: "1. Open Otto and connect to your daemon.",
|
|
211
|
+
"2. Web app: https://app.otto-code.me",
|
|
212
|
+
"3. Desktop app: https://github.com/Draek2077/otto-code/releases/latest",
|
|
213
|
+
"4. Docs: https://otto-code.me/docs",
|
|
214
|
+
'5. Example: otto run --output-schema schema.json "extract fields"',
|
|
215
|
+
];
|
|
216
|
+
const quickReferenceLines = [
|
|
217
|
+
"1. otto --help",
|
|
218
|
+
"2. otto ls",
|
|
219
|
+
'3. otto run "your prompt"',
|
|
220
|
+
"4. otto status",
|
|
221
|
+
`5. Daemon logs: ${daemonLogPath}`,
|
|
222
|
+
];
|
|
223
|
+
if (!richUi) {
|
|
224
|
+
console.log("");
|
|
225
|
+
console.log("Next steps:");
|
|
226
|
+
for (const line of nextStepsLines) {
|
|
227
|
+
console.log(line);
|
|
228
|
+
}
|
|
229
|
+
console.log("");
|
|
230
|
+
console.log("CLI quick reference:");
|
|
231
|
+
for (const line of quickReferenceLines) {
|
|
232
|
+
console.log(line);
|
|
233
|
+
}
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
renderNote(nextStepsLines.join("\n"), "Next steps");
|
|
237
|
+
renderNote(quickReferenceLines.join("\n"), "CLI quick reference");
|
|
238
|
+
}
|
|
239
|
+
export function onboardCommand() {
|
|
240
|
+
return new Command("onboard")
|
|
241
|
+
.description("Run first-time setup, start daemon, and print pairing instructions")
|
|
242
|
+
.option("--listen <listen>", "Listen target (host:port, port, or unix socket path)")
|
|
243
|
+
.option("--port <port>", "Port to listen on (default: 6868)")
|
|
244
|
+
.option("--home <path>", "Otto home directory (default: ~/.otto)")
|
|
245
|
+
.option("--no-relay", "Disable relay connection")
|
|
246
|
+
.option("--no-mcp", "Disable the Agent MCP HTTP endpoint")
|
|
247
|
+
.option("--hostnames <hosts>", 'Daemon hostnames (comma-separated, e.g. "myhost,.example.com" or "true" for any)')
|
|
248
|
+
.addOption(new Option("--allowed-hosts <hosts>").hideHelp())
|
|
249
|
+
.option("--timeout <seconds>", "Max time to wait for daemon readiness (default: 600)")
|
|
250
|
+
.option("--voice <mode>", "Voice setup mode: ask, enable, disable", "ask")
|
|
251
|
+
.action(async (options) => {
|
|
252
|
+
await runOnboard({
|
|
253
|
+
...options,
|
|
254
|
+
hostnames: options.hostnames ?? options.allowedHosts,
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
async function resolveAndPersistVoice(ottoHome, options) {
|
|
259
|
+
let persisted = loadPersistedConfig(ottoHome);
|
|
260
|
+
const persistedVoiceSelection = resolvePersistedVoiceSelection(persisted);
|
|
261
|
+
const shouldPrompt = options.voice === "ask" || options.voice === undefined;
|
|
262
|
+
let voiceEnabled;
|
|
263
|
+
try {
|
|
264
|
+
voiceEnabled =
|
|
265
|
+
shouldPrompt && persistedVoiceSelection !== null
|
|
266
|
+
? persistedVoiceSelection
|
|
267
|
+
: await resolveVoiceSelection(options.voice);
|
|
268
|
+
}
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error instanceof OnboardCancelledError) {
|
|
271
|
+
cancel("Onboarding cancelled.");
|
|
272
|
+
process.exit(0);
|
|
273
|
+
}
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
if (shouldPrompt && persistedVoiceSelection !== null) {
|
|
277
|
+
log.message(`Using saved voice setup from config (${voiceEnabled ? "enabled" : "disabled"}).`);
|
|
278
|
+
}
|
|
279
|
+
persisted = applyVoiceSelection(persisted, voiceEnabled);
|
|
280
|
+
savePersistedConfig(ottoHome, persisted);
|
|
281
|
+
return voiceEnabled;
|
|
282
|
+
}
|
|
283
|
+
async function ensureDaemonStarted(options, richUi) {
|
|
284
|
+
const stateBeforeStart = resolveLocalDaemonState({ home: options.home });
|
|
285
|
+
if (stateBeforeStart.running) {
|
|
286
|
+
log.message(`Daemon already running (PID ${stateBeforeStart.pidInfo?.pid ?? "unknown"}).`);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const startSpinner = richUi ? spinner() : null;
|
|
290
|
+
try {
|
|
291
|
+
if (startSpinner) {
|
|
292
|
+
startSpinner.start("Starting daemon...");
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
log.message("Starting daemon...");
|
|
296
|
+
}
|
|
297
|
+
const startup = await startLocalDaemonDetached(options);
|
|
298
|
+
if (startSpinner) {
|
|
299
|
+
startSpinner.stop(`Daemon started (PID ${startup.pid ?? "unknown"})`);
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
log.message(`Daemon started (PID ${startup.pid ?? "unknown"})`);
|
|
303
|
+
}
|
|
304
|
+
log.message(`Logs: ${startup.logPath}`);
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
308
|
+
if (startSpinner) {
|
|
309
|
+
startSpinner.error(message);
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
log.error(message);
|
|
313
|
+
}
|
|
314
|
+
process.exit(1);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
async function waitForDaemonReadyWithUi(args) {
|
|
318
|
+
const readySpinner = args.richUi ? spinner() : null;
|
|
319
|
+
try {
|
|
320
|
+
if (readySpinner) {
|
|
321
|
+
readySpinner.start("Waiting for daemon to become ready...");
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
log.message("Waiting for daemon to become ready...");
|
|
325
|
+
}
|
|
326
|
+
const readyState = await waitForDaemonReady({
|
|
327
|
+
home: args.home,
|
|
328
|
+
timeoutMs: args.timeoutMs,
|
|
329
|
+
onStatus: readySpinner ? (message) => readySpinner.message(message) : undefined,
|
|
330
|
+
});
|
|
331
|
+
if (readySpinner) {
|
|
332
|
+
readySpinner.stop(`Daemon ready on ${readyState.listen}`);
|
|
333
|
+
}
|
|
334
|
+
else {
|
|
335
|
+
log.message(`Daemon ready on ${readyState.listen}`);
|
|
336
|
+
}
|
|
337
|
+
return readyState;
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
341
|
+
if (readySpinner) {
|
|
342
|
+
readySpinner.error(message);
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
log.error(message);
|
|
346
|
+
}
|
|
347
|
+
return process.exit(1);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
export async function runOnboard(options) {
|
|
351
|
+
const richUi = process.stdin.isTTY && process.stdout.isTTY;
|
|
352
|
+
if (richUi) {
|
|
353
|
+
intro("Welcome to Otto");
|
|
354
|
+
}
|
|
355
|
+
if (options.listen && options.port) {
|
|
356
|
+
cancel("Cannot use --listen and --port together");
|
|
357
|
+
process.exit(1);
|
|
358
|
+
}
|
|
359
|
+
let timeoutMs = DEFAULT_READY_TIMEOUT_MS;
|
|
360
|
+
try {
|
|
361
|
+
timeoutMs = parseTimeoutMs(options.timeout);
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
365
|
+
cancel(message);
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
const ottoHome = resolveLocalOttoHome(options.home);
|
|
369
|
+
if (richUi) {
|
|
370
|
+
renderNote(ottoHome, "Otto home");
|
|
371
|
+
}
|
|
372
|
+
const voiceEnabled = await resolveAndPersistVoice(ottoHome, options);
|
|
373
|
+
const config = loadConfig(ottoHome, { cli: toCliOverrides(options) });
|
|
374
|
+
log.message(voiceEnabled
|
|
375
|
+
? "Voice features enabled. Local speech models will be downloaded automatically if missing."
|
|
376
|
+
: "Voice features disabled. Local speech models will not be downloaded.");
|
|
377
|
+
await ensureDaemonStarted(options, richUi);
|
|
378
|
+
await waitForDaemonReadyWithUi({
|
|
379
|
+
home: options.home ?? ottoHome,
|
|
380
|
+
timeoutMs,
|
|
381
|
+
richUi,
|
|
382
|
+
});
|
|
383
|
+
if (config.relayEnabled === false) {
|
|
384
|
+
log.warn("Relay is disabled; pairing offer is unavailable for this daemon.");
|
|
385
|
+
printNextSteps(null, ottoHome, richUi);
|
|
386
|
+
if (richUi) {
|
|
387
|
+
outro("Otto daemon is running.");
|
|
388
|
+
}
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
const pairing = await generateLocalPairingOffer({
|
|
392
|
+
ottoHome,
|
|
393
|
+
relayEnabled: config.relayEnabled,
|
|
394
|
+
relayEndpoint: config.relayEndpoint,
|
|
395
|
+
relayPublicEndpoint: config.relayPublicEndpoint,
|
|
396
|
+
relayUseTls: config.relayUseTls,
|
|
397
|
+
relayPublicUseTls: config.relayPublicUseTls,
|
|
398
|
+
appBaseUrl: config.appBaseUrl,
|
|
399
|
+
includeQr: true,
|
|
400
|
+
});
|
|
401
|
+
if (!pairing.url) {
|
|
402
|
+
log.warn("Relay pairing URL is unavailable for this daemon configuration.");
|
|
403
|
+
printNextSteps(null, ottoHome, richUi);
|
|
404
|
+
if (richUi) {
|
|
405
|
+
outro("Otto daemon is running.");
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
renderNote(pairing.qr ?? "QR is unavailable in this terminal. Use the pairing link below.", "Scan to pair");
|
|
410
|
+
renderNote(pairing.url, "Pairing link");
|
|
411
|
+
printNextSteps(pairing.url, ottoHome, richUi);
|
|
412
|
+
if (richUi) {
|
|
413
|
+
outro("Otto is ready!");
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
//# sourceMappingURL=onboard.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnProcess } from "@otto-code/server";
|
|
5
|
+
function findDesktopApp() {
|
|
6
|
+
if (process.platform === "darwin") {
|
|
7
|
+
const candidates = ["/Applications/Otto.app", path.join(homedir(), "Applications", "Otto.app")];
|
|
8
|
+
for (const candidate of candidates) {
|
|
9
|
+
if (existsSync(candidate)) {
|
|
10
|
+
return candidate;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
if (process.platform === "linux") {
|
|
16
|
+
const candidates = [
|
|
17
|
+
"/usr/bin/Otto",
|
|
18
|
+
"/opt/Otto/Otto",
|
|
19
|
+
path.join(homedir(), "Applications", "Otto.AppImage"),
|
|
20
|
+
];
|
|
21
|
+
for (const candidate of candidates) {
|
|
22
|
+
if (existsSync(candidate)) {
|
|
23
|
+
return candidate;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
if (process.platform === "win32") {
|
|
29
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
30
|
+
if (!localAppData) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const candidate = path.join(localAppData, "Programs", "Otto", "Otto.exe");
|
|
34
|
+
return existsSync(candidate) ? candidate : null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
function cleanEnvForDesktopLaunch() {
|
|
39
|
+
const env = { ...process.env };
|
|
40
|
+
// The CLI runs via ELECTRON_RUN_AS_NODE=1. On Linux/Windows the spawned
|
|
41
|
+
// desktop process inherits the env directly, so we must strip it or the
|
|
42
|
+
// desktop app would start as a bare Node process instead of Electron.
|
|
43
|
+
delete env.ELECTRON_RUN_AS_NODE;
|
|
44
|
+
delete env.ELECTRON_NO_ATTACH_CONSOLE;
|
|
45
|
+
delete env.OTTO_NODE_ENV;
|
|
46
|
+
return env;
|
|
47
|
+
}
|
|
48
|
+
function spawnDetached(command, args) {
|
|
49
|
+
spawnProcess(command, args, {
|
|
50
|
+
detached: true,
|
|
51
|
+
stdio: "ignore",
|
|
52
|
+
env: cleanEnvForDesktopLaunch(),
|
|
53
|
+
}).unref();
|
|
54
|
+
}
|
|
55
|
+
export async function openDesktopWithProject(projectPath) {
|
|
56
|
+
try {
|
|
57
|
+
if (process.env.OTTO_DESKTOP_CLI === "1") {
|
|
58
|
+
throw new Error("Cannot open a desktop project while running in desktop CLI passthrough mode.");
|
|
59
|
+
}
|
|
60
|
+
const desktopApp = findDesktopApp();
|
|
61
|
+
if (!desktopApp) {
|
|
62
|
+
throw new Error("Otto desktop app not found. Install it from https://github.com/Draek2077/otto-code/releases");
|
|
63
|
+
}
|
|
64
|
+
if (process.platform === "darwin") {
|
|
65
|
+
// -n forces a new instance even if the app is already running.
|
|
66
|
+
// The new instance hits requestSingleInstanceLock(), fails, and relays
|
|
67
|
+
// the argv to the first instance via the second-instance event.
|
|
68
|
+
// -g keeps the terminal in the foreground (better CLI UX).
|
|
69
|
+
// Without -n, macOS just activates the existing window and drops --args.
|
|
70
|
+
spawnDetached("open", ["-n", "-g", "-a", desktopApp, "--args", projectPath]);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
spawnDetached(desktopApp, [projectPath]);
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
process.stderr.write(`${message}\n`);
|
|
78
|
+
process.exitCode = 1;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=open.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { CommandOptions, ListResult, OutputSchema } from "../../output/index.js";
|
|
3
|
+
/** Permission response item for display */
|
|
4
|
+
export interface PermissionResponseItem {
|
|
5
|
+
requestId: string;
|
|
6
|
+
agentId: string;
|
|
7
|
+
agentShortId: string;
|
|
8
|
+
name: string;
|
|
9
|
+
result: string;
|
|
10
|
+
}
|
|
11
|
+
/** Schema for permit allow/deny output */
|
|
12
|
+
export declare const permitResponseSchema: OutputSchema<PermissionResponseItem>;
|
|
13
|
+
export type PermitAllowResult = ListResult<PermissionResponseItem>;
|
|
14
|
+
export interface PermitAllowOptions extends CommandOptions {
|
|
15
|
+
all?: boolean;
|
|
16
|
+
input?: string;
|
|
17
|
+
host?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function runAllowCommand(agentIdOrPrefix: string, reqId: string | undefined, options: PermitAllowOptions, _command: Command): Promise<PermitAllowResult>;
|
|
20
|
+
//# sourceMappingURL=allow.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { connectToDaemon, getDaemonHost } from "../../utils/client.js";
|
|
2
|
+
/** Schema for permit allow/deny output */
|
|
3
|
+
export const permitResponseSchema = {
|
|
4
|
+
idField: "requestId",
|
|
5
|
+
columns: [
|
|
6
|
+
{ header: "REQUEST ID", field: "requestId", width: 12 },
|
|
7
|
+
{ header: "AGENT", field: "agentShortId", width: 10 },
|
|
8
|
+
{ header: "TOOL", field: "name", width: 20 },
|
|
9
|
+
{
|
|
10
|
+
header: "RESULT",
|
|
11
|
+
field: "result",
|
|
12
|
+
width: 10,
|
|
13
|
+
color: (value) => {
|
|
14
|
+
if (value === "allowed")
|
|
15
|
+
return "green";
|
|
16
|
+
if (value === "denied")
|
|
17
|
+
return "red";
|
|
18
|
+
return undefined;
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
],
|
|
22
|
+
};
|
|
23
|
+
export async function runAllowCommand(agentIdOrPrefix, reqId, options, _command) {
|
|
24
|
+
const host = getDaemonHost({ host: options.host });
|
|
25
|
+
// No validation needed - if no reqId provided, allow all by default
|
|
26
|
+
// Parse input JSON if provided
|
|
27
|
+
let updatedInput;
|
|
28
|
+
if (options.input) {
|
|
29
|
+
try {
|
|
30
|
+
updatedInput = JSON.parse(options.input);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
const error = {
|
|
34
|
+
code: "INVALID_JSON",
|
|
35
|
+
message: `Invalid JSON for --input: ${err instanceof Error ? err.message : String(err)}`,
|
|
36
|
+
details: 'Provide valid JSON, e.g., --input \'{"key": "value"}\'',
|
|
37
|
+
};
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
let client;
|
|
42
|
+
try {
|
|
43
|
+
client = await connectToDaemon({ host: options.host });
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
47
|
+
const error = {
|
|
48
|
+
code: "DAEMON_NOT_RUNNING",
|
|
49
|
+
message: `Cannot connect to daemon at ${host}: ${message}`,
|
|
50
|
+
details: "Start the daemon with: otto daemon start",
|
|
51
|
+
};
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const fetchResult = await client.fetchAgent({ agentId: agentIdOrPrefix });
|
|
56
|
+
if (!fetchResult) {
|
|
57
|
+
await client.close();
|
|
58
|
+
const error = {
|
|
59
|
+
code: "AGENT_NOT_FOUND",
|
|
60
|
+
message: `Agent not found: ${agentIdOrPrefix}`,
|
|
61
|
+
details: 'Use "otto ls" to list available agents',
|
|
62
|
+
};
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
const agent = fetchResult.agent;
|
|
66
|
+
const resolvedAgentId = agent.id;
|
|
67
|
+
// Get pending permissions for this agent
|
|
68
|
+
const pendingPermissions = agent.pendingPermissions || [];
|
|
69
|
+
if (pendingPermissions.length === 0) {
|
|
70
|
+
await client.close();
|
|
71
|
+
const error = {
|
|
72
|
+
code: "NO_PENDING_PERMISSIONS",
|
|
73
|
+
message: `No pending permissions for agent ${agent.id.slice(0, 7)}`,
|
|
74
|
+
};
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
// Determine which permissions to allow
|
|
78
|
+
let permissionsToAllow;
|
|
79
|
+
if (!reqId || options.all) {
|
|
80
|
+
// Default: allow all pending permissions if no req_id specified
|
|
81
|
+
// --all flag is kept as an explicit alias for clarity
|
|
82
|
+
permissionsToAllow = pendingPermissions;
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Find permission by ID prefix
|
|
86
|
+
const permission = pendingPermissions.find((p) => p.id === reqId || p.id.startsWith(reqId));
|
|
87
|
+
if (!permission) {
|
|
88
|
+
await client.close();
|
|
89
|
+
const error = {
|
|
90
|
+
code: "PERMISSION_NOT_FOUND",
|
|
91
|
+
message: `Permission request not found: ${reqId}`,
|
|
92
|
+
details: `Available requests: ${pendingPermissions.map((p) => p.id.slice(0, 8)).join(", ")}`,
|
|
93
|
+
};
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
permissionsToAllow = [permission];
|
|
97
|
+
}
|
|
98
|
+
// Allow permissions
|
|
99
|
+
const results = await Promise.all(permissionsToAllow.map(async (permission) => {
|
|
100
|
+
await client.respondToPermission(resolvedAgentId, permission.id, {
|
|
101
|
+
behavior: "allow",
|
|
102
|
+
...(updatedInput ? { updatedInput } : {}),
|
|
103
|
+
});
|
|
104
|
+
return {
|
|
105
|
+
requestId: permission.id.slice(0, 8),
|
|
106
|
+
agentId: resolvedAgentId,
|
|
107
|
+
agentShortId: resolvedAgentId.slice(0, 7),
|
|
108
|
+
name: permission.name,
|
|
109
|
+
result: "allowed",
|
|
110
|
+
};
|
|
111
|
+
}));
|
|
112
|
+
await client.close();
|
|
113
|
+
return {
|
|
114
|
+
type: "list",
|
|
115
|
+
data: results,
|
|
116
|
+
schema: permitResponseSchema,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
await client.close().catch(() => { });
|
|
121
|
+
// Re-throw CommandErrors
|
|
122
|
+
if (err && typeof err === "object" && "code" in err) {
|
|
123
|
+
throw err;
|
|
124
|
+
}
|
|
125
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
126
|
+
const error = {
|
|
127
|
+
code: "ALLOW_PERMISSION_FAILED",
|
|
128
|
+
message: `Failed to allow permission: ${message}`,
|
|
129
|
+
};
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=allow.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { CommandOptions, ListResult } from "../../output/index.js";
|
|
3
|
+
import { type PermissionResponseItem } from "./allow.js";
|
|
4
|
+
export type PermitDenyResult = ListResult<PermissionResponseItem>;
|
|
5
|
+
export interface PermitDenyOptions extends CommandOptions {
|
|
6
|
+
all?: boolean;
|
|
7
|
+
message?: string;
|
|
8
|
+
interrupt?: boolean;
|
|
9
|
+
host?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function runDenyCommand(agentIdOrPrefix: string, reqId: string | undefined, options: PermitDenyOptions, _command: Command): Promise<PermitDenyResult>;
|
|
12
|
+
//# sourceMappingURL=deny.d.ts.map
|