@every-env/spiral-cli 0.1.0 → 1.0.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/README.md +54 -9
- package/package.json +5 -14
- package/src/api.ts +82 -474
- package/src/auth.ts +51 -131
- package/src/cli.ts +266 -884
- package/src/config.ts +30 -38
- package/src/output.ts +162 -0
- package/src/types.ts +87 -117
- package/src/attachments/index.ts +0 -174
- package/src/drafts/editor.ts +0 -105
- package/src/drafts/index.ts +0 -208
- package/src/notes/index.ts +0 -130
- package/src/styles/index.ts +0 -45
- package/src/suggestions/diff.ts +0 -33
- package/src/suggestions/index.ts +0 -205
- package/src/suggestions/parser.ts +0 -83
- package/src/tools/renderer.ts +0 -104
- package/src/workspaces/index.ts +0 -55
package/src/cli.ts
CHANGED
|
@@ -1,952 +1,334 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import { formatAttachmentsSummary, processAttachments } from "./attachments";
|
|
11
|
-
import { clearTokenCache, getAuthToken, sanitizeError } from "./auth";
|
|
12
|
-
import { config } from "./config";
|
|
3
|
+
import { parseArgs } from "util";
|
|
4
|
+
import { input } from "@inquirer/prompts";
|
|
5
|
+
import { theme, EXIT_CODES } from "./theme.js";
|
|
6
|
+
import { sanitizeError } from "./auth.js";
|
|
7
|
+
import * as auth from "./auth.js";
|
|
8
|
+
import * as api from "./api.js";
|
|
9
|
+
import * as output from "./output.js";
|
|
13
10
|
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "./drafts";
|
|
21
|
-
import { addNote, clearNotes, listNotes, removeNote } from "./notes";
|
|
22
|
-
import { listStyles } from "./styles";
|
|
23
|
-
import {
|
|
24
|
-
applySuggestion,
|
|
25
|
-
dismissSuggestion,
|
|
26
|
-
listPendingSuggestions,
|
|
27
|
-
previewSuggestion,
|
|
28
|
-
registerSuggestions,
|
|
29
|
-
} from "./suggestions";
|
|
30
|
-
import { EXIT_CODES, theme } from "./theme";
|
|
31
|
-
import { cleanupToolSpinners, handleToolCallUpdate } from "./tools/renderer";
|
|
32
|
-
import { ApiError, type Attachment, AuthenticationError, SpiralCliError } from "./types";
|
|
33
|
-
import { listWorkspaces } from "./workspaces";
|
|
34
|
-
|
|
35
|
-
// Configure marked for terminal output
|
|
36
|
-
marked.use(
|
|
37
|
-
markedTerminal({
|
|
38
|
-
heading: chalk.cyan.bold,
|
|
39
|
-
code: chalk.yellow,
|
|
40
|
-
codespan: chalk.bgBlack.yellow,
|
|
41
|
-
}),
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
const VERSION = "0.2.0";
|
|
45
|
-
|
|
46
|
-
// Parse CLI arguments
|
|
47
|
-
const { values, positionals } = parseArgs({
|
|
48
|
-
args: Bun.argv.slice(2),
|
|
49
|
-
options: {
|
|
50
|
-
help: { type: "boolean", short: "h" },
|
|
51
|
-
version: { type: "boolean", short: "v" },
|
|
52
|
-
session: { type: "string", short: "s" },
|
|
53
|
-
json: { type: "boolean" },
|
|
54
|
-
quiet: { type: "boolean", short: "q" },
|
|
55
|
-
new: { type: "boolean", short: "n" },
|
|
56
|
-
limit: { type: "string" },
|
|
57
|
-
debug: { type: "boolean", short: "d" },
|
|
58
|
-
// New options for extended features
|
|
59
|
-
attach: { type: "string", multiple: true, short: "a" },
|
|
60
|
-
style: { type: "string" },
|
|
61
|
-
workspace: { type: "string" },
|
|
62
|
-
force: { type: "boolean", short: "f" },
|
|
63
|
-
content: { type: "string" }, // For agent-native draft updates
|
|
64
|
-
title: { type: "string" }, // For draft title
|
|
65
|
-
versionId: { type: "string" }, // For version restore
|
|
66
|
-
},
|
|
67
|
-
allowPositionals: true,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// Enable debug mode via flag
|
|
71
|
-
if (values.debug) {
|
|
72
|
-
process.env.DEBUG = "1";
|
|
73
|
-
}
|
|
11
|
+
SpiralCliError,
|
|
12
|
+
AuthenticationError,
|
|
13
|
+
ApiError,
|
|
14
|
+
type GenerateRequest,
|
|
15
|
+
type GenerateResponse,
|
|
16
|
+
} from "./types.js";
|
|
74
17
|
|
|
75
|
-
|
|
76
|
-
let pendingAttachments: Attachment[] = [];
|
|
18
|
+
const VERSION = "1.0.0";
|
|
77
19
|
|
|
78
|
-
|
|
79
|
-
|
|
20
|
+
const HELP = `
|
|
21
|
+
${theme.heading("spiral")} — write with Spiral from your terminal
|
|
80
22
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
await notesCommand(positionals[1], positionals.slice(2).join(" "));
|
|
122
|
-
break;
|
|
123
|
-
case "suggestions":
|
|
124
|
-
await suggestionsCommand(positionals[1], positionals[2]);
|
|
125
|
-
break;
|
|
126
|
-
default:
|
|
127
|
-
console.error(theme.error(`Unknown command: ${command}`));
|
|
128
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
129
|
-
}
|
|
23
|
+
${theme.heading("Usage:")}
|
|
24
|
+
spiral write "a blog post about X" Create content
|
|
25
|
+
spiral write "refine tone" --session <id> Refine existing drafts
|
|
26
|
+
spiral styles List writing styles
|
|
27
|
+
spiral workspaces List workspaces
|
|
28
|
+
spiral sessions List conversations
|
|
29
|
+
spiral drafts --session <id> List drafts in a session
|
|
30
|
+
spiral quota Session quota info
|
|
31
|
+
spiral prime Agent discovery document
|
|
32
|
+
spiral auth login|status|logout Manage authentication
|
|
33
|
+
|
|
34
|
+
${theme.heading("Flags:")}
|
|
35
|
+
--json Machine-readable JSON output
|
|
36
|
+
--session <id> Session ID for multi-turn / refinement
|
|
37
|
+
--style <id> Writing style ID
|
|
38
|
+
--workspace <id> Workspace ID
|
|
39
|
+
--instant Skip clarifying questions
|
|
40
|
+
--num-drafts <n> Number of drafts (1-5, default 1)
|
|
41
|
+
--token <key> API key for auth login
|
|
42
|
+
--debug Show debug info on errors
|
|
43
|
+
--help, -h Show this help
|
|
44
|
+
--version, -v Show version
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
interface ParsedArgs {
|
|
48
|
+
command: string;
|
|
49
|
+
positional: string[];
|
|
50
|
+
flags: {
|
|
51
|
+
json: boolean;
|
|
52
|
+
session?: string;
|
|
53
|
+
style?: string;
|
|
54
|
+
workspace?: string;
|
|
55
|
+
instant: boolean;
|
|
56
|
+
numDrafts?: number;
|
|
57
|
+
token?: string;
|
|
58
|
+
debug: boolean;
|
|
59
|
+
help: boolean;
|
|
60
|
+
version: boolean;
|
|
61
|
+
limit?: number;
|
|
62
|
+
};
|
|
130
63
|
}
|
|
131
64
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const sessionId = values.session || null;
|
|
153
|
-
const outputJson = values.json;
|
|
154
|
-
const quiet = values.quiet;
|
|
155
|
-
const spinner = quiet ? null : ora("Sending...").start();
|
|
156
|
-
|
|
157
|
-
// Process attachments if provided
|
|
158
|
-
let attachments: Attachment[] = [];
|
|
159
|
-
if (values.attach && values.attach.length > 0) {
|
|
160
|
-
try {
|
|
161
|
-
attachments = await processAttachments(values.attach, { quiet });
|
|
162
|
-
if (!quiet && !outputJson) {
|
|
163
|
-
console.log(theme.info(`Attached: ${formatAttachmentsSummary(attachments)}`));
|
|
164
|
-
}
|
|
165
|
-
} catch (error) {
|
|
166
|
-
console.error(theme.error((error as Error).message));
|
|
167
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Performance: Use array accumulation (O(n) vs O(n²) string concat)
|
|
172
|
-
const chunks: string[] = [];
|
|
173
|
-
const thinkingChunks: string[] = [];
|
|
174
|
-
let finalSessionId = sessionId || "";
|
|
175
|
-
|
|
176
|
-
const controller = new AbortController();
|
|
177
|
-
|
|
178
|
-
// Handle Ctrl+C
|
|
179
|
-
process.on("SIGINT", () => {
|
|
180
|
-
controller.abort();
|
|
181
|
-
spinner?.stop();
|
|
182
|
-
cleanupToolSpinners();
|
|
183
|
-
if (outputJson) {
|
|
184
|
-
console.log(JSON.stringify({ status: "cancelled", partial: chunks.join("") }));
|
|
185
|
-
} else {
|
|
186
|
-
console.log(theme.warning("\n(Cancelled)"));
|
|
187
|
-
}
|
|
188
|
-
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
65
|
+
function parse(argv: string[]): ParsedArgs {
|
|
66
|
+
const { values, positionals } = parseArgs({
|
|
67
|
+
args: argv.slice(2),
|
|
68
|
+
options: {
|
|
69
|
+
json: { type: "boolean", default: false },
|
|
70
|
+
session: { type: "string" },
|
|
71
|
+
style: { type: "string" },
|
|
72
|
+
workspace: { type: "string" },
|
|
73
|
+
instant: { type: "boolean", default: false },
|
|
74
|
+
"num-drafts": { type: "string" },
|
|
75
|
+
token: { type: "string" },
|
|
76
|
+
debug: { type: "boolean", default: false },
|
|
77
|
+
help: { type: "boolean", short: "h", default: false },
|
|
78
|
+
version: { type: "boolean", short: "v", default: false },
|
|
79
|
+
limit: { type: "string" },
|
|
80
|
+
},
|
|
81
|
+
allowPositionals: true,
|
|
82
|
+
strict: false,
|
|
189
83
|
});
|
|
190
84
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
result: tool.result,
|
|
217
|
-
error: tool.error,
|
|
218
|
-
});
|
|
219
|
-
},
|
|
220
|
-
onSessionName: () => {},
|
|
221
|
-
onRetry: (info) => {
|
|
222
|
-
if (spinner) spinner.text = `Retrying (${info.attempt}/${info.max})...`;
|
|
223
|
-
},
|
|
224
|
-
onModelDowngrade: (from, to) => {
|
|
225
|
-
if (!quiet && !outputJson) {
|
|
226
|
-
console.log(theme.warning(`\nNote: Model downgraded from ${from} to ${to}`));
|
|
227
|
-
}
|
|
228
|
-
},
|
|
229
|
-
onComplete: (id) => {
|
|
230
|
-
finalSessionId = id;
|
|
231
|
-
},
|
|
232
|
-
onError: (error) => {
|
|
233
|
-
throw error;
|
|
234
|
-
},
|
|
235
|
-
},
|
|
236
|
-
controller.signal,
|
|
237
|
-
{
|
|
238
|
-
attachments: attachments.length > 0 ? attachments : undefined,
|
|
239
|
-
},
|
|
240
|
-
);
|
|
85
|
+
const command = positionals[0] ?? "";
|
|
86
|
+
const rest = positionals.slice(1);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
command,
|
|
90
|
+
positional: rest,
|
|
91
|
+
flags: {
|
|
92
|
+
json: values.json as boolean,
|
|
93
|
+
session: values.session as string | undefined,
|
|
94
|
+
style: values.style as string | undefined,
|
|
95
|
+
workspace: values.workspace as string | undefined,
|
|
96
|
+
instant: values.instant as boolean,
|
|
97
|
+
numDrafts: values["num-drafts"]
|
|
98
|
+
? Number.parseInt(values["num-drafts"] as string, 10)
|
|
99
|
+
: undefined,
|
|
100
|
+
token: values.token as string | undefined,
|
|
101
|
+
debug: values.debug as boolean,
|
|
102
|
+
help: values.help as boolean,
|
|
103
|
+
version: values.version as boolean,
|
|
104
|
+
limit: values.limit
|
|
105
|
+
? Number.parseInt(values.limit as string, 10)
|
|
106
|
+
: undefined,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
241
110
|
|
|
242
|
-
|
|
111
|
+
// ── Commands ──
|
|
243
112
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
console.log(theme.info(`\n${sugCount} suggestion(s) parsed. Use /suggestions to view.`));
|
|
248
|
-
}
|
|
113
|
+
async function cmdWrite(args: ParsedArgs): Promise<void> {
|
|
114
|
+
// Read prompt from positional args or stdin
|
|
115
|
+
let prompt = args.positional.join(" ").trim();
|
|
249
116
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
content,
|
|
256
|
-
thinking: thinkingChunks.join(""),
|
|
257
|
-
suggestions_count: sugCount,
|
|
258
|
-
}),
|
|
259
|
-
);
|
|
260
|
-
} else {
|
|
261
|
-
console.log(); // Newline after streaming
|
|
117
|
+
if (!prompt && !process.stdin.isTTY) {
|
|
118
|
+
// Read from piped stdin
|
|
119
|
+
const chunks: Buffer[] = [];
|
|
120
|
+
for await (const chunk of process.stdin) {
|
|
121
|
+
chunks.push(chunk as Buffer);
|
|
262
122
|
}
|
|
263
|
-
|
|
264
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
265
|
-
} catch (error) {
|
|
266
|
-
spinner?.stop();
|
|
267
|
-
cleanupToolSpinners();
|
|
268
|
-
handleError(error as Error);
|
|
123
|
+
prompt = Buffer.concat(chunks).toString("utf-8").trim();
|
|
269
124
|
}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Interactive REPL command
|
|
274
|
-
*/
|
|
275
|
-
async function chatCommand(): Promise<void> {
|
|
276
|
-
console.log(`${theme.heading("Spiral Chat")} (type /help for commands)\n`);
|
|
277
125
|
|
|
278
|
-
if (
|
|
279
|
-
|
|
126
|
+
if (!prompt && !args.flags.session) {
|
|
127
|
+
throw new SpiralCliError(
|
|
128
|
+
'Usage: spiral write "your prompt here"',
|
|
129
|
+
EXIT_CODES.INVALID_ARGS,
|
|
130
|
+
);
|
|
280
131
|
}
|
|
281
132
|
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const handleInput = async (input: string): Promise<void> => {
|
|
292
|
-
const trimmed = input.trim();
|
|
133
|
+
const req: GenerateRequest = {
|
|
134
|
+
prompt: prompt || undefined,
|
|
135
|
+
session_id: args.flags.session,
|
|
136
|
+
style_id: args.flags.style,
|
|
137
|
+
workspace_id: args.flags.workspace,
|
|
138
|
+
mode: args.flags.instant ? "instant" : "interactive",
|
|
139
|
+
num_drafts: args.flags.numDrafts,
|
|
140
|
+
};
|
|
293
141
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (newSession !== undefined) {
|
|
298
|
-
sessionId = newSession;
|
|
299
|
-
}
|
|
300
|
-
rl.prompt();
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
142
|
+
let resp: GenerateResponse = await output.withSpinner("Writing...", () =>
|
|
143
|
+
api.generate(req),
|
|
144
|
+
);
|
|
303
145
|
|
|
304
|
-
|
|
305
|
-
|
|
146
|
+
// Multi-turn loop: handle needs_input
|
|
147
|
+
while (resp.status === "needs_input") {
|
|
148
|
+
if (args.flags.json || !process.stdin.isTTY) {
|
|
149
|
+
// Non-interactive: return the needs_input response for caller to handle
|
|
150
|
+
output.outputJson(resp);
|
|
306
151
|
return;
|
|
307
152
|
}
|
|
308
153
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
currentController = new AbortController();
|
|
312
|
-
|
|
313
|
-
try {
|
|
314
|
-
await streamChat(
|
|
315
|
-
trimmed,
|
|
316
|
-
sessionId,
|
|
317
|
-
{
|
|
318
|
-
onChunk: (chunk) => {
|
|
319
|
-
if (spinner.isSpinning) {
|
|
320
|
-
spinner.stop();
|
|
321
|
-
process.stdout.write(theme.assistant("Spiral: "));
|
|
322
|
-
}
|
|
323
|
-
chunks.push(chunk);
|
|
324
|
-
process.stdout.write(chunk);
|
|
325
|
-
},
|
|
326
|
-
onThinking: (thought) => {
|
|
327
|
-
spinner.text = theme.thinking(`Thinking: ${thought.slice(0, 40)}...`);
|
|
328
|
-
},
|
|
329
|
-
onToolCall: (tool) => {
|
|
330
|
-
if (tool.status === "started") {
|
|
331
|
-
spinner.text = `Using ${tool.tool_name}...`;
|
|
332
|
-
}
|
|
333
|
-
},
|
|
334
|
-
onSessionName: (name) => {
|
|
335
|
-
// Could update terminal title here
|
|
336
|
-
if (process.env.DEBUG) {
|
|
337
|
-
console.log(theme.dim(`\n[Session: ${name}]`));
|
|
338
|
-
}
|
|
339
|
-
},
|
|
340
|
-
onRetry: (info) => {
|
|
341
|
-
spinner.text = theme.warning(`Retrying (${info.attempt}/${info.max})...`);
|
|
342
|
-
},
|
|
343
|
-
onModelDowngrade: (from, to) => {
|
|
344
|
-
console.log(theme.warning(`\nNote: Model changed from ${from} to ${to}`));
|
|
345
|
-
},
|
|
346
|
-
onComplete: (id) => {
|
|
347
|
-
sessionId = id;
|
|
348
|
-
},
|
|
349
|
-
onError: (error) => {
|
|
350
|
-
throw error;
|
|
351
|
-
},
|
|
352
|
-
},
|
|
353
|
-
currentController.signal,
|
|
354
|
-
);
|
|
355
|
-
|
|
356
|
-
// Register suggestions from response
|
|
357
|
-
const content = chunks.join("");
|
|
358
|
-
const sugCount = registerSuggestions(content);
|
|
359
|
-
if (sugCount > 0) {
|
|
360
|
-
console.log(theme.info(`\n${sugCount} suggestion(s) parsed. Use /suggestions to view.`));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
console.log("\n"); // Newline after response
|
|
364
|
-
} catch (error) {
|
|
365
|
-
if (!currentController.signal.aborted) {
|
|
366
|
-
spinner.fail(sanitizeError(error as Error));
|
|
367
|
-
}
|
|
368
|
-
}
|
|
154
|
+
// Show questions
|
|
155
|
+
output.formatGenerateResponse(resp);
|
|
369
156
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
rl.prompt();
|
|
373
|
-
};
|
|
157
|
+
// Prompt for answers
|
|
158
|
+
const answer = await input({ message: "Your response:" });
|
|
374
159
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
160
|
+
resp = await output.withSpinner("Writing...", () =>
|
|
161
|
+
api.generate({
|
|
162
|
+
session_id: resp.session_id,
|
|
163
|
+
responses: answer,
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
381
167
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
rl.prompt();
|
|
389
|
-
} else {
|
|
390
|
-
console.log(theme.dim("\nGoodbye!"));
|
|
391
|
-
process.exit(EXIT_CODES.SUCCESS);
|
|
392
|
-
}
|
|
393
|
-
});
|
|
168
|
+
if (args.flags.json) {
|
|
169
|
+
output.outputJson(resp);
|
|
170
|
+
} else {
|
|
171
|
+
output.formatGenerateResponse(resp);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
394
174
|
|
|
395
|
-
|
|
175
|
+
async function cmdStyles(args: ParsedArgs): Promise<void> {
|
|
176
|
+
const styles = await output.withSpinner("Loading styles...", () =>
|
|
177
|
+
api.getStyles(),
|
|
178
|
+
);
|
|
179
|
+
if (args.flags.json) {
|
|
180
|
+
output.outputJson(styles);
|
|
181
|
+
} else {
|
|
182
|
+
output.formatStyles(styles);
|
|
183
|
+
}
|
|
396
184
|
}
|
|
397
185
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
): Promise<string | null | undefined> {
|
|
407
|
-
const parts = cmd.slice(1).split(" ");
|
|
408
|
-
const command = parts[0];
|
|
409
|
-
const args = parts.slice(1);
|
|
410
|
-
|
|
411
|
-
switch (command) {
|
|
412
|
-
case "help":
|
|
413
|
-
console.log(`
|
|
414
|
-
${theme.heading("Commands:")}
|
|
415
|
-
/help Show this help
|
|
416
|
-
/exit Exit the chat
|
|
417
|
-
/clear Clear the screen
|
|
418
|
-
/history Show current session history
|
|
419
|
-
/sessions List all sessions
|
|
420
|
-
/new Start a new session
|
|
421
|
-
/session <id> Switch to different session
|
|
422
|
-
/debug Toggle debug mode
|
|
423
|
-
|
|
424
|
-
${theme.heading("Content Management:")}
|
|
425
|
-
/drafts List drafts in current session
|
|
426
|
-
/draft view <id> View a draft
|
|
427
|
-
/draft edit <id> Edit a draft in $EDITOR
|
|
428
|
-
/draft versions <id> Show version history
|
|
429
|
-
/styles List writing styles
|
|
430
|
-
/style <id> Set writing style for messages
|
|
431
|
-
/workspaces List workspaces
|
|
432
|
-
/workspace <id> Set workspace for new sessions
|
|
433
|
-
|
|
434
|
-
${theme.heading("Notes & Suggestions:")}
|
|
435
|
-
/note <text> Add a note to scratchpad
|
|
436
|
-
/notes List all notes
|
|
437
|
-
/notes clear Clear all notes
|
|
438
|
-
/suggestions List pending suggestions
|
|
439
|
-
/apply <id> Apply a suggestion
|
|
440
|
-
/dismiss <id> Dismiss a suggestion
|
|
441
|
-
|
|
442
|
-
${theme.heading("Attachments:")}
|
|
443
|
-
/attach <files...> Queue files for next message
|
|
444
|
-
`);
|
|
445
|
-
break;
|
|
446
|
-
case "exit":
|
|
447
|
-
case "quit":
|
|
448
|
-
console.log(theme.dim("Goodbye!"));
|
|
449
|
-
return process.exit(EXIT_CODES.SUCCESS);
|
|
450
|
-
case "clear":
|
|
451
|
-
console.clear();
|
|
452
|
-
break;
|
|
453
|
-
case "history":
|
|
454
|
-
if (sessionId) {
|
|
455
|
-
await historyCommand(sessionId);
|
|
456
|
-
} else {
|
|
457
|
-
console.log(theme.warning("No active session. Start a conversation first."));
|
|
458
|
-
}
|
|
459
|
-
break;
|
|
460
|
-
case "sessions":
|
|
461
|
-
await sessionsCommand();
|
|
462
|
-
break;
|
|
463
|
-
case "new":
|
|
464
|
-
console.log(theme.info("Starting new session..."));
|
|
465
|
-
return null; // Clear session ID
|
|
466
|
-
case "session":
|
|
467
|
-
if (args[0]) {
|
|
468
|
-
console.log(theme.info(`Switching to session ${args[0]}...`));
|
|
469
|
-
return args[0];
|
|
470
|
-
}
|
|
471
|
-
console.log(theme.error("Usage: /session <id>"));
|
|
472
|
-
break;
|
|
473
|
-
case "debug":
|
|
474
|
-
process.env.DEBUG = process.env.DEBUG ? "" : "1";
|
|
475
|
-
console.log(theme.info(`Debug mode: ${process.env.DEBUG ? "ON" : "OFF"}`));
|
|
476
|
-
break;
|
|
477
|
-
// New commands
|
|
478
|
-
case "drafts":
|
|
479
|
-
if (sessionId) {
|
|
480
|
-
await listDrafts(sessionId, { json: false });
|
|
481
|
-
} else {
|
|
482
|
-
console.log(theme.warning("No active session."));
|
|
483
|
-
}
|
|
484
|
-
break;
|
|
485
|
-
case "draft": {
|
|
486
|
-
if (!sessionId) {
|
|
487
|
-
console.log(theme.warning("No active session."));
|
|
488
|
-
break;
|
|
489
|
-
}
|
|
490
|
-
const [draftAction, draftArg] = args;
|
|
491
|
-
switch (draftAction) {
|
|
492
|
-
case "view":
|
|
493
|
-
if (draftArg) await viewDraft(sessionId, draftArg, { json: false });
|
|
494
|
-
else console.log(theme.error("Usage: /draft view <id>"));
|
|
495
|
-
break;
|
|
496
|
-
case "edit":
|
|
497
|
-
if (draftArg) await editDraft(sessionId, draftArg, { json: false });
|
|
498
|
-
else console.log(theme.error("Usage: /draft edit <id>"));
|
|
499
|
-
break;
|
|
500
|
-
case "versions":
|
|
501
|
-
if (draftArg) await listVersions(sessionId, draftArg, { json: false });
|
|
502
|
-
else console.log(theme.error("Usage: /draft versions <id>"));
|
|
503
|
-
break;
|
|
504
|
-
default:
|
|
505
|
-
console.log(theme.error("Usage: /draft [view|edit|versions] <id>"));
|
|
506
|
-
}
|
|
507
|
-
break;
|
|
508
|
-
}
|
|
509
|
-
case "styles":
|
|
510
|
-
await listStyles({ json: false });
|
|
511
|
-
break;
|
|
512
|
-
case "style":
|
|
513
|
-
if (args[0]) {
|
|
514
|
-
config.set("currentStyleId", args[0]);
|
|
515
|
-
console.log(theme.info(`Style set to ${args[0]}`));
|
|
516
|
-
} else {
|
|
517
|
-
const current = config.get("currentStyleId");
|
|
518
|
-
if (current) {
|
|
519
|
-
console.log(theme.info(`Current style: ${current}`));
|
|
520
|
-
} else {
|
|
521
|
-
console.log(theme.error("Usage: /style <id>"));
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
break;
|
|
525
|
-
case "workspaces":
|
|
526
|
-
await listWorkspaces({ json: false });
|
|
527
|
-
break;
|
|
528
|
-
case "workspace":
|
|
529
|
-
if (args[0]) {
|
|
530
|
-
config.set("currentWorkspaceId", args[0]);
|
|
531
|
-
console.log(theme.info(`Workspace set to ${args[0]}`));
|
|
532
|
-
console.log(theme.warning("Note: Start a /new session to use this workspace."));
|
|
533
|
-
} else {
|
|
534
|
-
const current = config.get("currentWorkspaceId");
|
|
535
|
-
if (current) {
|
|
536
|
-
console.log(theme.info(`Current workspace: ${current}`));
|
|
537
|
-
} else {
|
|
538
|
-
console.log(theme.error("Usage: /workspace <id>"));
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
break;
|
|
542
|
-
case "note":
|
|
543
|
-
if (args.length > 0) {
|
|
544
|
-
addNote(args.join(" "));
|
|
545
|
-
} else {
|
|
546
|
-
console.log(theme.error("Usage: /note <text>"));
|
|
547
|
-
}
|
|
548
|
-
break;
|
|
549
|
-
case "notes":
|
|
550
|
-
if (args[0] === "clear") {
|
|
551
|
-
await clearNotes({ json: false });
|
|
552
|
-
} else {
|
|
553
|
-
listNotes({ json: false });
|
|
554
|
-
}
|
|
555
|
-
break;
|
|
556
|
-
case "suggestions":
|
|
557
|
-
listPendingSuggestions({ json: false });
|
|
558
|
-
break;
|
|
559
|
-
case "apply":
|
|
560
|
-
if (args[0] && sessionId) {
|
|
561
|
-
await applySuggestion(sessionId, args[0], { json: false });
|
|
562
|
-
} else if (!sessionId) {
|
|
563
|
-
console.log(theme.warning("No active session."));
|
|
564
|
-
} else {
|
|
565
|
-
console.log(theme.error("Usage: /apply <suggestion-id>"));
|
|
566
|
-
}
|
|
567
|
-
break;
|
|
568
|
-
case "dismiss":
|
|
569
|
-
if (args[0]) {
|
|
570
|
-
dismissSuggestion(args[0], { json: false });
|
|
571
|
-
} else {
|
|
572
|
-
console.log(theme.error("Usage: /dismiss <suggestion-id>"));
|
|
573
|
-
}
|
|
574
|
-
break;
|
|
575
|
-
case "attach":
|
|
576
|
-
if (args.length > 0) {
|
|
577
|
-
try {
|
|
578
|
-
pendingAttachments = await processAttachments(args);
|
|
579
|
-
console.log(
|
|
580
|
-
theme.success(`Queued ${pendingAttachments.length} file(s) for next message.`),
|
|
581
|
-
);
|
|
582
|
-
} catch (error) {
|
|
583
|
-
console.log(theme.error((error as Error).message));
|
|
584
|
-
}
|
|
585
|
-
} else {
|
|
586
|
-
console.log(theme.error("Usage: /attach <file1> [file2] ..."));
|
|
587
|
-
}
|
|
588
|
-
break;
|
|
589
|
-
default:
|
|
590
|
-
console.log(theme.error(`Unknown command: ${command}`));
|
|
186
|
+
async function cmdWorkspaces(args: ParsedArgs): Promise<void> {
|
|
187
|
+
const workspaces = await output.withSpinner("Loading workspaces...", () =>
|
|
188
|
+
api.getWorkspaces(),
|
|
189
|
+
);
|
|
190
|
+
if (args.flags.json) {
|
|
191
|
+
output.outputJson(workspaces);
|
|
192
|
+
} else {
|
|
193
|
+
output.formatWorkspaces(workspaces);
|
|
591
194
|
}
|
|
592
|
-
return undefined;
|
|
593
195
|
}
|
|
594
196
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
197
|
+
async function cmdSessions(args: ParsedArgs): Promise<void> {
|
|
198
|
+
const data = await output.withSpinner("Loading sessions...", () =>
|
|
199
|
+
api.getConversations(),
|
|
200
|
+
);
|
|
201
|
+
if (args.flags.json) {
|
|
202
|
+
output.outputJson(data.sessions);
|
|
203
|
+
} else {
|
|
204
|
+
output.formatConversations(data.sessions);
|
|
603
205
|
}
|
|
604
|
-
await listDrafts(sessionId, { json: values.json });
|
|
605
206
|
}
|
|
606
207
|
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
*/
|
|
610
|
-
async function draftCommand(action?: string, draftId?: string): Promise<void> {
|
|
611
|
-
const sessionId = values.session;
|
|
208
|
+
async function cmdDrafts(args: ParsedArgs): Promise<void> {
|
|
209
|
+
const sessionId = args.flags.session;
|
|
612
210
|
if (!sessionId) {
|
|
613
|
-
|
|
614
|
-
|
|
211
|
+
throw new SpiralCliError(
|
|
212
|
+
"Usage: spiral drafts --session <id>",
|
|
213
|
+
EXIT_CODES.INVALID_ARGS,
|
|
214
|
+
);
|
|
615
215
|
}
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
216
|
+
const drafts = await output.withSpinner("Loading drafts...", () =>
|
|
217
|
+
api.getSessionDrafts(sessionId),
|
|
218
|
+
);
|
|
219
|
+
if (args.flags.json) {
|
|
220
|
+
output.outputJson(drafts);
|
|
221
|
+
} else {
|
|
222
|
+
output.formatSessionDrafts(drafts);
|
|
620
223
|
}
|
|
224
|
+
}
|
|
621
225
|
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
break;
|
|
631
|
-
case "update":
|
|
632
|
-
// Agent-native update with --content flag
|
|
633
|
-
if (!values.content) {
|
|
634
|
-
console.error(theme.error("--content required for update"));
|
|
635
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
636
|
-
}
|
|
637
|
-
await updateDraftContent(sessionId, draftId, values.content, {
|
|
638
|
-
json: values.json,
|
|
639
|
-
title: values.title,
|
|
640
|
-
});
|
|
641
|
-
break;
|
|
642
|
-
case "versions":
|
|
643
|
-
await listVersions(sessionId, draftId, { json: values.json, limit });
|
|
644
|
-
break;
|
|
645
|
-
case "restore":
|
|
646
|
-
if (!values.versionId) {
|
|
647
|
-
console.error(theme.error("--versionId required for restore"));
|
|
648
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
649
|
-
}
|
|
650
|
-
await restoreVersion(sessionId, draftId, values.versionId, { json: values.json });
|
|
651
|
-
break;
|
|
652
|
-
default:
|
|
653
|
-
console.error(theme.error(`Unknown draft action: ${action}`));
|
|
654
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
226
|
+
async function cmdQuota(args: ParsedArgs): Promise<void> {
|
|
227
|
+
const quota = await output.withSpinner("Loading quota...", () =>
|
|
228
|
+
api.getQuota(),
|
|
229
|
+
);
|
|
230
|
+
if (args.flags.json) {
|
|
231
|
+
output.outputJson(quota);
|
|
232
|
+
} else {
|
|
233
|
+
output.formatQuota(quota);
|
|
655
234
|
}
|
|
656
235
|
}
|
|
657
236
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
case undefined:
|
|
667
|
-
listNotes(opts);
|
|
668
|
-
break;
|
|
669
|
-
case "add":
|
|
670
|
-
if (!content) {
|
|
671
|
-
console.error(theme.error("Usage: spiral notes add <content>"));
|
|
672
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
673
|
-
}
|
|
674
|
-
addNote(content, undefined, opts);
|
|
675
|
-
break;
|
|
676
|
-
case "clear":
|
|
677
|
-
await clearNotes(opts);
|
|
678
|
-
break;
|
|
679
|
-
case "remove":
|
|
680
|
-
if (!content) {
|
|
681
|
-
console.error(theme.error("Usage: spiral notes remove <id>"));
|
|
682
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
683
|
-
}
|
|
684
|
-
removeNote(content, opts);
|
|
685
|
-
break;
|
|
686
|
-
default:
|
|
687
|
-
// Treat as adding a note if no recognized action
|
|
688
|
-
addNote([action, content].filter(Boolean).join(" "), undefined, opts);
|
|
237
|
+
async function cmdPrime(args: ParsedArgs): Promise<void> {
|
|
238
|
+
const doc = await output.withSpinner("Loading prime...", () =>
|
|
239
|
+
api.getPrime(),
|
|
240
|
+
);
|
|
241
|
+
if (args.flags.json) {
|
|
242
|
+
output.outputJson({ content: doc });
|
|
243
|
+
} else {
|
|
244
|
+
output.outputMarkdown(doc);
|
|
689
245
|
}
|
|
690
246
|
}
|
|
691
247
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const sessionId = values.session;
|
|
698
|
-
|
|
699
|
-
switch (action) {
|
|
700
|
-
case "list":
|
|
701
|
-
case undefined:
|
|
702
|
-
listPendingSuggestions(opts);
|
|
703
|
-
break;
|
|
704
|
-
case "preview":
|
|
705
|
-
if (!suggestionId) {
|
|
706
|
-
console.error(theme.error("Usage: spiral suggestions preview <id>"));
|
|
707
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
708
|
-
}
|
|
709
|
-
await previewSuggestion(suggestionId, opts);
|
|
248
|
+
async function cmdAuth(args: ParsedArgs): Promise<void> {
|
|
249
|
+
const sub = args.positional[0];
|
|
250
|
+
switch (sub) {
|
|
251
|
+
case "login":
|
|
252
|
+
await auth.login(args.flags.token);
|
|
710
253
|
break;
|
|
711
|
-
case "
|
|
712
|
-
|
|
713
|
-
console.error(theme.error("Usage: spiral suggestions apply <id>"));
|
|
714
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
715
|
-
}
|
|
716
|
-
if (!sessionId) {
|
|
717
|
-
console.error(theme.error("--session required for apply"));
|
|
718
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
719
|
-
}
|
|
720
|
-
await applySuggestion(sessionId, suggestionId, opts);
|
|
254
|
+
case "status":
|
|
255
|
+
auth.showStatus();
|
|
721
256
|
break;
|
|
722
|
-
case "
|
|
723
|
-
|
|
724
|
-
console.error(theme.error("Usage: spiral suggestions dismiss <id>"));
|
|
725
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
726
|
-
}
|
|
727
|
-
dismissSuggestion(suggestionId, opts);
|
|
257
|
+
case "logout":
|
|
258
|
+
auth.logout();
|
|
728
259
|
break;
|
|
729
260
|
default:
|
|
730
|
-
console.
|
|
731
|
-
|
|
261
|
+
console.log("Usage: spiral auth login|status|logout");
|
|
262
|
+
break;
|
|
732
263
|
}
|
|
733
264
|
}
|
|
734
265
|
|
|
735
|
-
|
|
736
|
-
* List sessions command (supports --json)
|
|
737
|
-
*/
|
|
738
|
-
async function sessionsCommand(): Promise<void> {
|
|
739
|
-
const spinner = values.quiet ? null : ora("Fetching sessions...").start();
|
|
740
|
-
|
|
741
|
-
try {
|
|
742
|
-
const conversations = await fetchConversations();
|
|
743
|
-
spinner?.succeed("Sessions loaded");
|
|
744
|
-
|
|
745
|
-
if (values.json) {
|
|
746
|
-
console.log(JSON.stringify(conversations));
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
console.log(theme.heading("\nYour Sessions:\n"));
|
|
751
|
-
|
|
752
|
-
const limit = Number.parseInt(values.limit || "20", 10);
|
|
753
|
-
for (const conv of conversations.slice(0, limit)) {
|
|
754
|
-
const date = new Date(conv.created_at).toLocaleDateString();
|
|
755
|
-
console.log(
|
|
756
|
-
`${theme.dim(conv.session_id.slice(0, 8))} ${chalk.white(conv.session_name || "Untitled")} ${theme.dim(`(${date})`)}`,
|
|
757
|
-
);
|
|
758
|
-
}
|
|
266
|
+
// ── Main ──
|
|
759
267
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
}
|
|
763
|
-
} catch (error) {
|
|
764
|
-
spinner?.stop();
|
|
765
|
-
handleError(error as Error);
|
|
766
|
-
}
|
|
767
|
-
}
|
|
268
|
+
async function main(): Promise<void> {
|
|
269
|
+
const args = parse(process.argv);
|
|
768
270
|
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
async function historyCommand(conversationId?: string): Promise<void> {
|
|
773
|
-
if (!conversationId) {
|
|
774
|
-
console.error(theme.error("Usage: spiral history <session-id>"));
|
|
775
|
-
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
271
|
+
if (args.flags.version) {
|
|
272
|
+
console.log(VERSION);
|
|
273
|
+
return;
|
|
776
274
|
}
|
|
777
275
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
const messages = await fetchMessages(conversationId);
|
|
782
|
-
spinner?.succeed("History loaded");
|
|
783
|
-
|
|
784
|
-
if (values.json) {
|
|
785
|
-
console.log(JSON.stringify(messages));
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
const limit = Number.parseInt(values.limit || "50", 10);
|
|
790
|
-
console.log(theme.heading(`\nSession History (${messages.length} messages):\n`));
|
|
791
|
-
|
|
792
|
-
for (const msg of messages.slice(-limit)) {
|
|
793
|
-
const role = msg.role === "user" ? theme.user("You") : theme.assistant("Spiral");
|
|
794
|
-
const content = msg.content || "";
|
|
795
|
-
const preview = content.slice(0, 200);
|
|
796
|
-
console.log(`${role}: ${preview}${content.length > 200 ? "..." : ""}\n`);
|
|
797
|
-
}
|
|
798
|
-
} catch (error) {
|
|
799
|
-
spinner?.stop();
|
|
800
|
-
handleError(error as Error);
|
|
276
|
+
if (args.flags.help || !args.command) {
|
|
277
|
+
console.log(HELP);
|
|
278
|
+
return;
|
|
801
279
|
}
|
|
802
|
-
}
|
|
803
280
|
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
console.log(
|
|
814
|
-
JSON.stringify({
|
|
815
|
-
status: "authenticated",
|
|
816
|
-
token_preview: `${token.slice(0, 20)}...`,
|
|
817
|
-
}),
|
|
818
|
-
);
|
|
819
|
-
} else {
|
|
820
|
-
console.log(theme.success("Authenticated"));
|
|
821
|
-
if (process.env.DEBUG) {
|
|
822
|
-
console.log(theme.dim(`Token: ${token.slice(0, 20)}...`));
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
} catch (error) {
|
|
826
|
-
if (values.json) {
|
|
827
|
-
console.log(
|
|
828
|
-
JSON.stringify({
|
|
829
|
-
status: "unauthenticated",
|
|
830
|
-
error: (error as Error).message,
|
|
831
|
-
}),
|
|
832
|
-
);
|
|
833
|
-
} else {
|
|
834
|
-
console.log(theme.error(`Not authenticated: ${(error as Error).message}`));
|
|
835
|
-
}
|
|
836
|
-
process.exit(EXIT_CODES.AUTH_ERROR);
|
|
837
|
-
}
|
|
281
|
+
switch (args.command) {
|
|
282
|
+
case "write":
|
|
283
|
+
await cmdWrite(args);
|
|
284
|
+
break;
|
|
285
|
+
case "styles":
|
|
286
|
+
await cmdStyles(args);
|
|
287
|
+
break;
|
|
288
|
+
case "workspaces":
|
|
289
|
+
await cmdWorkspaces(args);
|
|
838
290
|
break;
|
|
839
|
-
case "
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
);
|
|
291
|
+
case "sessions":
|
|
292
|
+
await cmdSessions(args);
|
|
293
|
+
break;
|
|
294
|
+
case "drafts":
|
|
295
|
+
await cmdDrafts(args);
|
|
296
|
+
break;
|
|
297
|
+
case "quota":
|
|
298
|
+
await cmdQuota(args);
|
|
299
|
+
break;
|
|
300
|
+
case "prime":
|
|
301
|
+
await cmdPrime(args);
|
|
302
|
+
break;
|
|
303
|
+
case "auth":
|
|
304
|
+
await cmdAuth(args);
|
|
844
305
|
break;
|
|
845
306
|
default:
|
|
846
|
-
console.
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
spiral auth clear Clear cached token
|
|
850
|
-
`);
|
|
307
|
+
console.error(theme.error(`Unknown command: ${args.command}`));
|
|
308
|
+
console.log(HELP);
|
|
309
|
+
process.exit(EXIT_CODES.INVALID_ARGS);
|
|
851
310
|
}
|
|
852
311
|
}
|
|
853
312
|
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
${theme.heading("Content Management:")}
|
|
869
|
-
spiral styles [--json] List writing styles
|
|
870
|
-
spiral workspaces [--json] List workspaces
|
|
871
|
-
spiral drafts --session <id> [--json] List drafts in a session
|
|
872
|
-
spiral draft <action> <id> --session <id> [--json]
|
|
873
|
-
view View draft content
|
|
874
|
-
edit Edit in $EDITOR
|
|
875
|
-
update Update with --content (agent-native)
|
|
876
|
-
versions View version history
|
|
877
|
-
restore Restore with --versionId
|
|
878
|
-
|
|
879
|
-
${theme.heading("Notes & Suggestions:")}
|
|
880
|
-
spiral notes [list|add|clear|remove] Manage local notes
|
|
881
|
-
spiral suggestions [list|preview|apply|dismiss] <id>
|
|
882
|
-
|
|
883
|
-
${theme.heading("Send Options:")}
|
|
884
|
-
--attach, -a <files...> Attach files to message
|
|
885
|
-
--style <id> Use writing style
|
|
886
|
-
--workspace <id> Use workspace context
|
|
887
|
-
|
|
888
|
-
${theme.heading("General Options:")}
|
|
889
|
-
--help, -h Show this help
|
|
890
|
-
--version, -v Show version
|
|
891
|
-
--session, -s Session ID to resume
|
|
892
|
-
--json Output as JSON (for scripting/agents)
|
|
893
|
-
--quiet, -q Suppress spinners and progress
|
|
894
|
-
--new, -n Start new session
|
|
895
|
-
--limit Limit results
|
|
896
|
-
--debug, -d Enable debug output
|
|
897
|
-
--force, -f Skip confirmations
|
|
898
|
-
|
|
899
|
-
${theme.heading("Examples:")}
|
|
900
|
-
spiral send "Write a haiku" --json
|
|
901
|
-
spiral send "Analyze this" --attach data.csv
|
|
902
|
-
spiral chat --session abc123
|
|
903
|
-
spiral drafts --session abc123
|
|
904
|
-
spiral draft edit draft-id --session abc123
|
|
905
|
-
spiral draft update draft-id --session abc123 --content "New content"
|
|
906
|
-
spiral notes add "Remember to check X"
|
|
907
|
-
spiral suggestions apply sug-123 --session abc123
|
|
908
|
-
|
|
909
|
-
${theme.heading("Exit Codes:")}
|
|
910
|
-
0 Success
|
|
911
|
-
1 General error
|
|
912
|
-
2 Authentication failed
|
|
913
|
-
3 API error
|
|
914
|
-
4 Network error
|
|
915
|
-
5 Invalid arguments
|
|
916
|
-
|
|
917
|
-
${theme.heading("Environment Variables:")}
|
|
918
|
-
SPIRAL_API_URL Override API endpoint
|
|
919
|
-
SPIRAL_TOKEN Provide auth token directly
|
|
920
|
-
EDITOR Editor for draft editing (default: vi)
|
|
921
|
-
DEBUG Enable verbose debug logging
|
|
922
|
-
|
|
923
|
-
${theme.heading("Authentication:")}
|
|
924
|
-
spiral-cli extracts your session from Safari automatically.
|
|
925
|
-
Make sure you're logged in at ${theme.info("https://app.writewithspiral.com")}
|
|
926
|
-
`);
|
|
927
|
-
}
|
|
928
|
-
|
|
929
|
-
/**
|
|
930
|
-
* Handle errors with proper exit codes
|
|
931
|
-
*/
|
|
932
|
-
function handleError(error: Error): never {
|
|
933
|
-
const message = sanitizeError(error);
|
|
934
|
-
|
|
935
|
-
if (values.json) {
|
|
936
|
-
console.log(JSON.stringify({ status: "error", error: message }));
|
|
937
|
-
} else {
|
|
938
|
-
console.error(theme.error("Error:"), message);
|
|
313
|
+
main().catch((err) => {
|
|
314
|
+
if (err instanceof SpiralCliError) {
|
|
315
|
+
if (!(err instanceof AuthenticationError) || !process.argv.includes("--json")) {
|
|
316
|
+
console.error(theme.error(err.message));
|
|
317
|
+
}
|
|
318
|
+
if (process.argv.includes("--json")) {
|
|
319
|
+
output.outputJson({
|
|
320
|
+
error: err.name,
|
|
321
|
+
message: err.message,
|
|
322
|
+
...(err instanceof ApiError ? { statusCode: err.statusCode } : {}),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
process.exit(err.exitCode);
|
|
939
326
|
}
|
|
940
327
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
} else if (error instanceof SpiralCliError) {
|
|
946
|
-
process.exit(error.exitCode);
|
|
328
|
+
const msg = sanitizeError(err);
|
|
329
|
+
console.error(theme.error(`Unexpected error: ${msg}`));
|
|
330
|
+
if (process.argv.includes("--debug")) {
|
|
331
|
+
console.error(err);
|
|
947
332
|
}
|
|
948
333
|
process.exit(EXIT_CODES.GENERAL_ERROR);
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
// Run
|
|
952
|
-
main().catch(handleError);
|
|
334
|
+
});
|