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