starcite 0.0.1

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/dist/index.js ADDED
@@ -0,0 +1,1078 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import {
5
+ createStarciteClient,
6
+ normalizeBaseUrl,
7
+ StarciteApiError
8
+ } from "@starcite/sdk";
9
+ import { Command, InvalidArgumentError } from "commander";
10
+ import { createConsola } from "consola";
11
+ import { z as z2 } from "zod";
12
+
13
+ // src/store.ts
14
+ import { randomUUID } from "crypto";
15
+ import { mkdir, writeFile } from "fs/promises";
16
+ import { homedir, hostname } from "os";
17
+ import { join, resolve } from "path";
18
+ import Conf from "conf";
19
+ import { cosmiconfig, defaultLoaders } from "cosmiconfig";
20
+ import { lock } from "proper-lockfile";
21
+ import { parse as parseToml } from "toml";
22
+ import { z } from "zod";
23
+ var DEFAULT_CONFIG_DIRECTORY_NAME = ".starcite";
24
+ var CONFIG_JSON_FILENAME = "config.json";
25
+ var CONFIG_TOML_FILENAME = "config.toml";
26
+ var CREDENTIALS_FILENAME = "credentials";
27
+ var IDENTITY_FILENAME = "identity";
28
+ var STATE_FILENAME = "state";
29
+ var STATE_LOCK_FILENAME = ".state.lock";
30
+ var TILDE_PREFIX_REGEX = /^~(?=\/|$)/;
31
+ var ConfigFileSchema = z.object({
32
+ baseUrl: z.string().optional(),
33
+ base_url: z.string().optional(),
34
+ producerId: z.string().optional(),
35
+ producer_id: z.string().optional(),
36
+ apiKey: z.string().optional(),
37
+ api_key: z.string().optional()
38
+ }).passthrough();
39
+ var IdentityFileSchema = z.object({
40
+ producerId: z.string().trim().min(1),
41
+ hostname: z.string().trim().min(1),
42
+ uuid: z.string().uuid(),
43
+ createdAt: z.string()
44
+ });
45
+ var StateFileSchema = z.object({
46
+ nextSeqByContext: z.record(z.number().int().positive()).default({})
47
+ });
48
+ var CredentialsFileSchema = z.object({
49
+ apiKey: z.string().trim().min(1).optional()
50
+ });
51
+ function trimString(value) {
52
+ const trimmed = value?.trim();
53
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
54
+ }
55
+ function normalizeConfig(input) {
56
+ const parsed = ConfigFileSchema.safeParse(input);
57
+ if (!parsed.success) {
58
+ return {};
59
+ }
60
+ return {
61
+ baseUrl: trimString(parsed.data.baseUrl ?? parsed.data.base_url),
62
+ producerId: trimString(parsed.data.producerId ?? parsed.data.producer_id),
63
+ apiKey: trimString(parsed.data.apiKey ?? parsed.data.api_key)
64
+ };
65
+ }
66
+ function defaultConfigDirectory() {
67
+ const home = homedir();
68
+ if (home.trim().length > 0) {
69
+ return join(home, DEFAULT_CONFIG_DIRECTORY_NAME);
70
+ }
71
+ return resolve(DEFAULT_CONFIG_DIRECTORY_NAME);
72
+ }
73
+ function resolveConfigDir(input) {
74
+ const configured = trimString(input) ?? trimString(process.env.STARCITE_HOME);
75
+ const withTilde = configured?.startsWith("~") ? configured.replace(TILDE_PREFIX_REGEX, homedir()) : configured;
76
+ return resolve(withTilde ?? defaultConfigDirectory());
77
+ }
78
+ function buildSeqContextKey(baseUrl, sessionId, producerId) {
79
+ return `${baseUrl}::${sessionId}::${producerId}`;
80
+ }
81
+ var StarciteCliStore = class {
82
+ directory;
83
+ lockPath;
84
+ configExplorer = cosmiconfig("starcite", {
85
+ cache: false,
86
+ searchStrategy: "none",
87
+ searchPlaces: [CONFIG_JSON_FILENAME, CONFIG_TOML_FILENAME],
88
+ loaders: {
89
+ ...defaultLoaders,
90
+ ".toml": (_filepath, content) => parseToml(content)
91
+ }
92
+ });
93
+ identityStore;
94
+ credentialsStore;
95
+ stateStore;
96
+ constructor(directory) {
97
+ this.directory = directory;
98
+ this.lockPath = join(directory, STATE_LOCK_FILENAME);
99
+ this.identityStore = new Conf({
100
+ cwd: directory,
101
+ clearInvalidConfig: true,
102
+ configName: IDENTITY_FILENAME,
103
+ fileExtension: "json"
104
+ });
105
+ this.credentialsStore = new Conf({
106
+ cwd: directory,
107
+ clearInvalidConfig: true,
108
+ configName: CREDENTIALS_FILENAME,
109
+ fileExtension: "json",
110
+ defaults: {}
111
+ });
112
+ this.stateStore = new Conf({
113
+ cwd: directory,
114
+ clearInvalidConfig: true,
115
+ configName: STATE_FILENAME,
116
+ fileExtension: "json",
117
+ defaults: { nextSeqByContext: {} }
118
+ });
119
+ }
120
+ async readConfig() {
121
+ await this.ensureConfigDirectory();
122
+ const result = await this.configExplorer.search(this.directory);
123
+ if (!result) {
124
+ return {};
125
+ }
126
+ return normalizeConfig(result.config);
127
+ }
128
+ async writeConfig(config) {
129
+ await this.ensureConfigDirectory();
130
+ const normalized = normalizeConfig(config);
131
+ const serialized = {};
132
+ if (normalized.baseUrl) {
133
+ serialized.baseUrl = normalized.baseUrl;
134
+ }
135
+ if (normalized.producerId) {
136
+ serialized.producerId = normalized.producerId;
137
+ }
138
+ if (normalized.apiKey) {
139
+ serialized.apiKey = normalized.apiKey;
140
+ }
141
+ await writeFile(
142
+ join(this.directory, CONFIG_JSON_FILENAME),
143
+ `${JSON.stringify(serialized, null, 2)}
144
+ `,
145
+ "utf8"
146
+ );
147
+ }
148
+ async updateConfig(patch) {
149
+ const current = await this.readConfig();
150
+ const merged = normalizeConfig({
151
+ ...current,
152
+ ...patch
153
+ });
154
+ await this.writeConfig(merged);
155
+ return merged;
156
+ }
157
+ async readApiKey() {
158
+ const fromEnv = trimString(process.env.STARCITE_API_KEY);
159
+ if (fromEnv) {
160
+ return fromEnv;
161
+ }
162
+ const parsed = CredentialsFileSchema.safeParse(this.credentialsStore.store);
163
+ const fromCredentials = parsed.success ? trimString(parsed.data.apiKey) : void 0;
164
+ if (fromCredentials) {
165
+ return fromCredentials;
166
+ }
167
+ const config = await this.readConfig();
168
+ return trimString(config.apiKey);
169
+ }
170
+ async saveApiKey(apiKey) {
171
+ await this.ensureConfigDirectory();
172
+ const normalized = trimString(apiKey);
173
+ if (!normalized) {
174
+ throw new Error("API key cannot be empty");
175
+ }
176
+ this.credentialsStore.set("apiKey", normalized);
177
+ }
178
+ async clearApiKey() {
179
+ await this.ensureConfigDirectory();
180
+ this.credentialsStore.delete("apiKey");
181
+ }
182
+ async resolveProducerId(explicitProducerId) {
183
+ const explicit = trimString(explicitProducerId);
184
+ if (explicit) {
185
+ return explicit;
186
+ }
187
+ const fromEnv = trimString(process.env.STARCITE_PRODUCER_ID);
188
+ if (fromEnv) {
189
+ return fromEnv;
190
+ }
191
+ const config = await this.readConfig();
192
+ const fromConfig = trimString(config.producerId);
193
+ if (fromConfig) {
194
+ return fromConfig;
195
+ }
196
+ const identity = await this.readOrCreateIdentity();
197
+ return identity.producerId;
198
+ }
199
+ async withStateLock(action) {
200
+ await this.ensureConfigDirectory();
201
+ const release = await lock(this.directory, {
202
+ lockfilePath: this.lockPath,
203
+ realpath: false,
204
+ retries: {
205
+ retries: 50,
206
+ minTimeout: 20,
207
+ maxTimeout: 60
208
+ }
209
+ });
210
+ try {
211
+ return await action();
212
+ } finally {
213
+ await release();
214
+ }
215
+ }
216
+ readNextSeq(contextKey) {
217
+ const state = this.readState();
218
+ return Promise.resolve(state.nextSeqByContext[contextKey] ?? 1);
219
+ }
220
+ bumpNextSeq(contextKey, usedSeq) {
221
+ const state = this.readState();
222
+ const nextSeqByContext = {
223
+ ...state.nextSeqByContext,
224
+ [contextKey]: Math.max(
225
+ state.nextSeqByContext[contextKey] ?? 1,
226
+ usedSeq + 1
227
+ )
228
+ };
229
+ this.stateStore.set("nextSeqByContext", nextSeqByContext);
230
+ return Promise.resolve();
231
+ }
232
+ readState() {
233
+ const parsed = StateFileSchema.safeParse(this.stateStore.store);
234
+ if (parsed.success) {
235
+ return parsed.data;
236
+ }
237
+ this.stateStore.clear();
238
+ return { nextSeqByContext: {} };
239
+ }
240
+ readOrCreateIdentity() {
241
+ const parsed = IdentityFileSchema.safeParse(this.identityStore.store);
242
+ if (parsed.success) {
243
+ return parsed.data;
244
+ }
245
+ const host = hostname();
246
+ const uuid = randomUUID();
247
+ const identity = {
248
+ producerId: `cli:${host}:${uuid}`,
249
+ hostname: host,
250
+ uuid,
251
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
252
+ };
253
+ this.identityStore.store = identity;
254
+ return identity;
255
+ }
256
+ async ensureConfigDirectory() {
257
+ await mkdir(this.directory, { recursive: true });
258
+ }
259
+ };
260
+
261
+ // src/up.ts
262
+ import { spawn } from "child_process";
263
+ import { mkdir as mkdir2, stat, writeFile as writeFile2 } from "fs/promises";
264
+ import { join as join2 } from "path";
265
+ import {
266
+ cancel as clackCancel,
267
+ confirm as clackConfirm,
268
+ password as clackPassword,
269
+ text as clackText,
270
+ isCancel
271
+ } from "@clack/prompts";
272
+ var DEFAULT_API_PORT = 45187;
273
+ var DEFAULT_DB_PORT = 5433;
274
+ var MIN_PORT = 1;
275
+ var MAX_PORT = 65535;
276
+ var RUNTIME_DIRECTORY_NAME = "runtime";
277
+ var DEFAULT_COMPOSE_FILE = `services:
278
+ app:
279
+ image: \${STARCITE_IMAGE:-ghcr.io/fastpaca/starcite:latest}
280
+ depends_on:
281
+ db:
282
+ condition: service_healthy
283
+ environment:
284
+ SECRET_KEY_BASE: \${SECRET_KEY_BASE:-xuQnOFm6sH5Qdd7x4WJv5smuG2Xf2nG0BL8rJ4yX6HnKGeTjo6n8r5hQKsxNkZWz}
285
+ PHX_HOST: \${PHX_HOST:-localhost}
286
+ PORT: 4000
287
+ DATABASE_URL: \${DATABASE_URL:-ecto://postgres:postgres@db:5432/starcite_dev}
288
+ MIGRATE_ON_BOOT: \${MIGRATE_ON_BOOT:-true}
289
+ DNS_CLUSTER_QUERY: \${DNS_CLUSTER_QUERY:-}
290
+ DNS_CLUSTER_NODE_BASENAME: \${DNS_CLUSTER_NODE_BASENAME:-starcite}
291
+ DNS_POLL_INTERVAL_MS: \${DNS_POLL_INTERVAL_MS:-5000}
292
+ ports:
293
+ - "\${STARCITE_API_PORT:-45187}:4000"
294
+ restart: unless-stopped
295
+
296
+ db:
297
+ image: postgres:15
298
+ environment:
299
+ POSTGRES_DB: starcite_dev
300
+ POSTGRES_USER: postgres
301
+ POSTGRES_PASSWORD: postgres
302
+ healthcheck:
303
+ test: ["CMD", "pg_isready", "-U", "postgres"]
304
+ interval: 10s
305
+ timeout: 5s
306
+ retries: 5
307
+ ports:
308
+ - "\${STARCITE_DB_PORT:-5433}:5432"
309
+ volumes:
310
+ - db-data:/var/lib/postgresql/data
311
+ restart: unless-stopped
312
+
313
+ volumes:
314
+ db-data:
315
+ `;
316
+ function parsePort(value, optionName) {
317
+ const parsed = Number(value);
318
+ if (!Number.isInteger(parsed) || parsed < MIN_PORT || parsed > MAX_PORT) {
319
+ throw new Error(
320
+ `${optionName} must be an integer between ${MIN_PORT} and ${MAX_PORT}`
321
+ );
322
+ }
323
+ return parsed;
324
+ }
325
+ function baseUrlPort(baseUrl) {
326
+ try {
327
+ const parsed = new URL(baseUrl);
328
+ if (parsed.port) {
329
+ return parsePort(parsed.port, "base URL port");
330
+ }
331
+ } catch {
332
+ return DEFAULT_API_PORT;
333
+ }
334
+ return DEFAULT_API_PORT;
335
+ }
336
+ async function ensureSuccess(runCommand, command, args) {
337
+ const result = await runCommand(command, args);
338
+ return result.code === 0;
339
+ }
340
+ async function ensureDockerReady(logger, runCommand) {
341
+ const hasDocker = await ensureSuccess(runCommand, "docker", ["--version"]);
342
+ if (!hasDocker) {
343
+ logger.error("You don't have Docker installed, please install it.");
344
+ throw new Error("Docker is required to run this command.");
345
+ }
346
+ const hasDockerCompose = await ensureSuccess(runCommand, "docker", [
347
+ "compose",
348
+ "version"
349
+ ]);
350
+ if (!hasDockerCompose) {
351
+ logger.error(
352
+ "Docker Compose is not available. Install Docker Compose and retry."
353
+ );
354
+ throw new Error("Docker Compose is required to run this command.");
355
+ }
356
+ const daemonRunning = await ensureSuccess(runCommand, "docker", ["info"]);
357
+ if (!daemonRunning) {
358
+ logger.error("Docker is installed but the daemon is not running.");
359
+ throw new Error("Start Docker and retry.");
360
+ }
361
+ }
362
+ async function selectApiPort(baseUrl, options, prompt) {
363
+ if (options.port !== void 0) {
364
+ return options.port;
365
+ }
366
+ const fallbackPort = baseUrlPort(baseUrl);
367
+ if (options.yes) {
368
+ return fallbackPort;
369
+ }
370
+ while (true) {
371
+ const answer = await prompt.input(
372
+ "What port do you want it on?",
373
+ `${fallbackPort}`
374
+ );
375
+ try {
376
+ return parsePort(answer, "port");
377
+ } catch (error) {
378
+ if (error instanceof Error) {
379
+ }
380
+ }
381
+ }
382
+ }
383
+ function runtimeDirectory(store) {
384
+ return join2(store.directory, RUNTIME_DIRECTORY_NAME);
385
+ }
386
+ async function runtimeDirectoryExists(store) {
387
+ try {
388
+ const result = await stat(runtimeDirectory(store));
389
+ return result.isDirectory();
390
+ } catch {
391
+ return false;
392
+ }
393
+ }
394
+ async function writeComposeFiles(store, options) {
395
+ const directory = runtimeDirectory(store);
396
+ await mkdir2(directory, { recursive: true });
397
+ await writeFile2(
398
+ join2(directory, "docker-compose.yml"),
399
+ DEFAULT_COMPOSE_FILE,
400
+ "utf8"
401
+ );
402
+ const envLines = [
403
+ `STARCITE_API_PORT=${options.apiPort}`,
404
+ `STARCITE_DB_PORT=${options.dbPort}`
405
+ ];
406
+ if (options.image?.trim()) {
407
+ envLines.push(`STARCITE_IMAGE=${options.image.trim()}`);
408
+ }
409
+ await writeFile2(join2(directory, ".env"), `${envLines.join("\n")}
410
+ `, "utf8");
411
+ return directory;
412
+ }
413
+ function parsePortOption(value, optionName) {
414
+ try {
415
+ return parsePort(value, optionName);
416
+ } catch (error) {
417
+ if (error instanceof Error) {
418
+ throw new Error(error.message);
419
+ }
420
+ throw error;
421
+ }
422
+ }
423
+ async function runUpWizard(input) {
424
+ const { baseUrl, logger, options, prompt, runCommand, store } = input;
425
+ await ensureDockerReady(logger, runCommand);
426
+ const confirmed = options.yes ? true : await prompt.confirm(
427
+ "Are you sure you want to create the docker containers?",
428
+ true
429
+ );
430
+ if (!confirmed) {
431
+ logger.info("Cancelled.");
432
+ return;
433
+ }
434
+ const apiPort = await selectApiPort(baseUrl, options, prompt);
435
+ const dbPort = options.dbPort ?? DEFAULT_DB_PORT;
436
+ const composeDirectory = await writeComposeFiles(store, {
437
+ apiPort,
438
+ dbPort,
439
+ image: options.image
440
+ });
441
+ const result = await runCommand("docker", ["compose", "up", "-d"], {
442
+ cwd: composeDirectory
443
+ });
444
+ if (result.code !== 0) {
445
+ const message = result.stderr || result.stdout || "docker compose up failed";
446
+ throw new Error(message.trim());
447
+ }
448
+ logger.info(`Starcite is starting on http://localhost:${apiPort}`);
449
+ logger.info(`Compose files are in ${composeDirectory}`);
450
+ }
451
+ async function runDownWizard(input) {
452
+ const { logger, options, prompt, runCommand, store } = input;
453
+ await ensureDockerReady(logger, runCommand);
454
+ const hasRuntimeDirectory = await runtimeDirectoryExists(store);
455
+ if (!hasRuntimeDirectory) {
456
+ logger.info(
457
+ `No Starcite runtime found at ${runtimeDirectory(
458
+ store
459
+ )}. Nothing to tear down.`
460
+ );
461
+ return;
462
+ }
463
+ const removeVolumes = options.volumes ?? true;
464
+ const confirmed = options.yes ? true : await prompt.confirm(
465
+ removeVolumes ? "Are you sure you want to stop and delete Starcite containers and volumes?" : "Are you sure you want to stop Starcite containers?",
466
+ false
467
+ );
468
+ if (!confirmed) {
469
+ logger.info("Cancelled.");
470
+ return;
471
+ }
472
+ const args = ["compose", "down", "--remove-orphans"];
473
+ if (removeVolumes) {
474
+ args.push("-v");
475
+ }
476
+ const result = await runCommand("docker", args, {
477
+ cwd: runtimeDirectory(store)
478
+ });
479
+ if (result.code !== 0) {
480
+ const message = result.stderr || result.stdout || "docker compose down failed";
481
+ throw new Error(message.trim());
482
+ }
483
+ logger.info("Starcite containers stopped.");
484
+ if (removeVolumes) {
485
+ logger.info("Starcite volumes removed.");
486
+ }
487
+ }
488
+ function createDefaultPrompt() {
489
+ const assertInteractive = () => {
490
+ if (!(process.stdin.isTTY && process.stdout.isTTY)) {
491
+ throw new Error(
492
+ "Interactive mode requires a TTY. Re-run with explicit options (for example: --yes, --endpoint, --api-key)."
493
+ );
494
+ }
495
+ };
496
+ return {
497
+ async confirm(message, defaultValue = true) {
498
+ assertInteractive();
499
+ const answer = await clackConfirm({
500
+ message,
501
+ initialValue: defaultValue,
502
+ input: process.stdin,
503
+ output: process.stdout
504
+ });
505
+ if (isCancel(answer)) {
506
+ clackCancel("Cancelled.");
507
+ return false;
508
+ }
509
+ return answer;
510
+ },
511
+ async input(message, defaultValue = "") {
512
+ assertInteractive();
513
+ const answer = await clackText({
514
+ message,
515
+ defaultValue,
516
+ placeholder: defaultValue || void 0,
517
+ input: process.stdin,
518
+ output: process.stdout
519
+ });
520
+ if (isCancel(answer)) {
521
+ clackCancel("Cancelled.");
522
+ throw new Error("Cancelled.");
523
+ }
524
+ const normalized = answer.trim();
525
+ return normalized || defaultValue;
526
+ },
527
+ async password(message) {
528
+ assertInteractive();
529
+ const answer = await clackPassword({
530
+ message,
531
+ input: process.stdin,
532
+ output: process.stdout
533
+ });
534
+ if (isCancel(answer)) {
535
+ clackCancel("Cancelled.");
536
+ throw new Error("Cancelled.");
537
+ }
538
+ return answer.trim();
539
+ }
540
+ };
541
+ }
542
+ var defaultCommandRunner = (command, args, options) => new Promise((resolve2) => {
543
+ const child = spawn(command, args, {
544
+ cwd: options?.cwd,
545
+ stdio: ["ignore", "pipe", "pipe"]
546
+ });
547
+ let stdout = "";
548
+ let stderr = "";
549
+ child.stdout?.on("data", (chunk) => {
550
+ stdout += chunk.toString();
551
+ });
552
+ child.stderr?.on("data", (chunk) => {
553
+ stderr += chunk.toString();
554
+ });
555
+ child.on("error", (error) => {
556
+ const message = error.message || "command failed to start";
557
+ resolve2({ code: 127, stdout, stderr: `${stderr}${message}` });
558
+ });
559
+ child.on("close", (code) => {
560
+ resolve2({ code: code ?? 1, stdout, stderr });
561
+ });
562
+ });
563
+
564
+ // src/cli.ts
565
+ var defaultLogger = createConsola();
566
+ var nonNegativeIntegerSchema = z2.coerce.number().int().nonnegative();
567
+ var positiveIntegerSchema = z2.coerce.number().int().positive();
568
+ var jsonObjectSchema = z2.record(z2.unknown());
569
+ var TRAILING_SLASHES_REGEX = /\/+$/;
570
+ function parseNonNegativeInteger(value, optionName) {
571
+ const parsed = nonNegativeIntegerSchema.safeParse(value);
572
+ if (!parsed.success) {
573
+ throw new InvalidArgumentError(
574
+ `${optionName} must be a non-negative integer`
575
+ );
576
+ }
577
+ return parsed.data;
578
+ }
579
+ function parsePositiveInteger(value, optionName) {
580
+ const parsed = positiveIntegerSchema.safeParse(value);
581
+ if (!parsed.success) {
582
+ throw new InvalidArgumentError(`${optionName} must be a positive integer`);
583
+ }
584
+ return parsed.data;
585
+ }
586
+ function parsePort2(value, optionName) {
587
+ return parsePortOption(value, optionName);
588
+ }
589
+ function parseEndpoint(value, optionName) {
590
+ const endpoint = trimString2(value);
591
+ if (!endpoint) {
592
+ throw new InvalidArgumentError(`${optionName} cannot be empty`);
593
+ }
594
+ let parsed;
595
+ try {
596
+ parsed = new URL(endpoint);
597
+ } catch {
598
+ throw new InvalidArgumentError(`${optionName} must be a valid URL`);
599
+ }
600
+ if (!(parsed.protocol === "http:" || parsed.protocol === "https:")) {
601
+ throw new InvalidArgumentError(
602
+ `${optionName} must use http:// or https://`
603
+ );
604
+ }
605
+ return endpoint.replace(TRAILING_SLASHES_REGEX, "");
606
+ }
607
+ function parseConfigSetKey(value) {
608
+ const normalized = value.trim().toLowerCase();
609
+ if (["endpoint", "base-url", "base_url"].includes(normalized)) {
610
+ return "endpoint";
611
+ }
612
+ if (["producer-id", "producer_id"].includes(normalized)) {
613
+ return "producer-id";
614
+ }
615
+ if (["api-key", "api_key"].includes(normalized)) {
616
+ return "api-key";
617
+ }
618
+ throw new InvalidArgumentError(
619
+ "config key must be one of: endpoint, producer-id, api-key"
620
+ );
621
+ }
622
+ function parseJsonOption(value, schema, optionName, invalidShapeMessage) {
623
+ let parsed;
624
+ try {
625
+ parsed = JSON.parse(value);
626
+ } catch {
627
+ throw new InvalidArgumentError(`${optionName} must be valid JSON`);
628
+ }
629
+ const result = schema.safeParse(parsed);
630
+ if (!result.success) {
631
+ throw new InvalidArgumentError(
632
+ `${optionName} must be ${invalidShapeMessage}`
633
+ );
634
+ }
635
+ return result.data;
636
+ }
637
+ function parseJsonObject(value, optionName) {
638
+ return parseJsonOption(value, jsonObjectSchema, optionName, "a JSON object");
639
+ }
640
+ function parseEventRefs(value) {
641
+ return parseJsonObject(value, "--refs");
642
+ }
643
+ function parseSessionMetadataFilters(value) {
644
+ const parsed = parseJsonObject(value, "--metadata");
645
+ const filters = {};
646
+ for (const [key, rawValue] of Object.entries(parsed)) {
647
+ if (key.trim().length === 0) {
648
+ throw new InvalidArgumentError("--metadata keys must be non-empty");
649
+ }
650
+ if (typeof rawValue !== "string") {
651
+ throw new InvalidArgumentError("--metadata values must be strings");
652
+ }
653
+ filters[key] = rawValue;
654
+ }
655
+ return filters;
656
+ }
657
+ function getGlobalOptions(command) {
658
+ return command.optsWithGlobals();
659
+ }
660
+ function trimString2(value) {
661
+ const trimmed = value?.trim();
662
+ return trimmed && trimmed.length > 0 ? trimmed : void 0;
663
+ }
664
+ function resolveBaseUrl(config, options) {
665
+ const defaultBaseUrl = `http://localhost:${DEFAULT_API_PORT}`;
666
+ return trimString2(options.baseUrl) ?? trimString2(process.env.STARCITE_BASE_URL) ?? trimString2(config.baseUrl) ?? defaultBaseUrl;
667
+ }
668
+ async function resolveGlobalOptions(command) {
669
+ const options = getGlobalOptions(command);
670
+ const configDir = resolveConfigDir(options.configDir);
671
+ const store = new StarciteCliStore(configDir);
672
+ const config = await store.readConfig();
673
+ const apiKey = trimString2(options.token) ?? await store.readApiKey();
674
+ return {
675
+ baseUrl: resolveBaseUrl(config, options),
676
+ apiKey,
677
+ json: options.json,
678
+ store
679
+ };
680
+ }
681
+ async function promptForEndpoint(prompt, defaultEndpoint) {
682
+ while (true) {
683
+ const answer = await prompt.input("Starcite endpoint URL", defaultEndpoint);
684
+ try {
685
+ return parseEndpoint(answer, "endpoint");
686
+ } catch {
687
+ }
688
+ }
689
+ }
690
+ async function promptForApiKey(prompt) {
691
+ const message = "Paste your Starcite API key";
692
+ const answer = prompt.password ? await prompt.password(message) : await prompt.input(message, "");
693
+ return trimString2(answer) ?? "";
694
+ }
695
+ function formatTailEvent(event) {
696
+ const actorLabel = event.agent ?? event.actor;
697
+ if (event.text) {
698
+ return `[${actorLabel}] ${event.text}`;
699
+ }
700
+ return `[${actorLabel}] ${JSON.stringify(event.payload)}`;
701
+ }
702
+ function buildProgram(deps = {}) {
703
+ const createClient = deps.createClient ?? ((baseUrl, apiKey) => createStarciteClient({
704
+ baseUrl,
705
+ apiKey
706
+ }));
707
+ const logger = deps.logger ?? defaultLogger;
708
+ const prompt = deps.prompt ?? createDefaultPrompt();
709
+ const runCommand = deps.runCommand ?? defaultCommandRunner;
710
+ const program = new Command();
711
+ program.name("starcite").description("Starcite CLI").showHelpAfterError().option("-u, --base-url <url>", "Starcite API base URL").option("-k, --token <token>", "Starcite API key / service JWT").option(
712
+ "--config-dir <path>",
713
+ "Starcite CLI config directory (default: ~/.starcite)"
714
+ ).option("--json", "Output JSON");
715
+ program.command("init").description("Initialize Starcite CLI config for a remote instance").option("--endpoint <url>", "Starcite endpoint URL").option("--api-key <key>", "API key to store").option("-y, --yes", "Skip prompts and only use provided options").action(async function initAction(options) {
716
+ const { baseUrl, json, store } = await resolveGlobalOptions(this);
717
+ const defaultEndpoint = parseEndpoint(baseUrl, "endpoint");
718
+ let endpoint = defaultEndpoint;
719
+ if (options.endpoint) {
720
+ endpoint = parseEndpoint(options.endpoint, "--endpoint");
721
+ } else if (!options.yes) {
722
+ endpoint = await promptForEndpoint(prompt, defaultEndpoint);
723
+ }
724
+ await store.updateConfig({ baseUrl: endpoint });
725
+ let apiKey = trimString2(options.apiKey);
726
+ if (!(apiKey || options.yes)) {
727
+ apiKey = await promptForApiKey(prompt);
728
+ }
729
+ if (apiKey) {
730
+ await store.saveApiKey(apiKey);
731
+ await store.updateConfig({ apiKey: void 0 });
732
+ }
733
+ if (json) {
734
+ logger.info(
735
+ JSON.stringify(
736
+ {
737
+ configDir: store.directory,
738
+ endpoint,
739
+ apiKeySaved: Boolean(apiKey)
740
+ },
741
+ null,
742
+ 2
743
+ )
744
+ );
745
+ return;
746
+ }
747
+ logger.info(`Initialized Starcite CLI in ${store.directory}`);
748
+ logger.info(`Endpoint set to ${endpoint}`);
749
+ if (apiKey) {
750
+ logger.info("API key saved. You can now run create/append/tail.");
751
+ } else {
752
+ logger.info("API key not set. Run `starcite auth login` when ready.");
753
+ }
754
+ });
755
+ program.command("config").description("Manage CLI configuration").addCommand(
756
+ new Command("set").description("Set a configuration value").argument("<key>", "endpoint | producer-id | api-key").argument("<value>", "value to store").action(async function configSetAction(key, value) {
757
+ const { store } = await resolveGlobalOptions(this);
758
+ const parsedKey = parseConfigSetKey(key);
759
+ if (parsedKey === "endpoint") {
760
+ const endpoint = parseEndpoint(value, "endpoint");
761
+ await store.updateConfig({ baseUrl: endpoint });
762
+ logger.info(`Endpoint set to ${endpoint}`);
763
+ return;
764
+ }
765
+ if (parsedKey === "producer-id") {
766
+ const producerId = trimString2(value);
767
+ if (!producerId) {
768
+ throw new InvalidArgumentError("producer-id cannot be empty");
769
+ }
770
+ await store.updateConfig({ producerId });
771
+ logger.info(`Producer ID set to ${producerId}`);
772
+ return;
773
+ }
774
+ await store.saveApiKey(value);
775
+ await store.updateConfig({ apiKey: void 0 });
776
+ logger.info("API key saved.");
777
+ })
778
+ ).addCommand(
779
+ new Command("show").description("Show current configuration").action(async function configShowAction() {
780
+ const { baseUrl, store } = await resolveGlobalOptions(this);
781
+ const config = await store.readConfig();
782
+ const apiKey = await store.readApiKey();
783
+ const fromEnv = trimString2(process.env.STARCITE_API_KEY);
784
+ let apiKeySource = "unset";
785
+ if (fromEnv) {
786
+ apiKeySource = "env";
787
+ } else if (apiKey) {
788
+ apiKeySource = "stored";
789
+ }
790
+ logger.info(
791
+ JSON.stringify(
792
+ {
793
+ endpoint: config.baseUrl ?? baseUrl,
794
+ producerId: config.producerId ?? null,
795
+ apiKey: apiKey ? "***" : null,
796
+ apiKeySource,
797
+ configDir: store.directory
798
+ },
799
+ null,
800
+ 2
801
+ )
802
+ );
803
+ })
804
+ );
805
+ program.command("auth").description("Manage API key authentication").addCommand(
806
+ new Command("login").description("Save an API key for authenticated requests").option("--api-key <key>", "API key to store").action(async function authLoginAction(options) {
807
+ const { store } = await resolveGlobalOptions(this);
808
+ let apiKey = trimString2(options.apiKey);
809
+ if (!apiKey) {
810
+ apiKey = await promptForApiKey(prompt);
811
+ }
812
+ if (!apiKey) {
813
+ throw new Error("API key cannot be empty");
814
+ }
815
+ await store.saveApiKey(apiKey);
816
+ await store.updateConfig({ apiKey: void 0 });
817
+ logger.info("API key saved.");
818
+ })
819
+ ).addCommand(
820
+ new Command("logout").description("Remove the saved API key").action(async function authLogoutAction() {
821
+ const { store } = await resolveGlobalOptions(this);
822
+ await store.clearApiKey();
823
+ logger.info("Saved API key removed.");
824
+ })
825
+ ).addCommand(
826
+ new Command("status").description("Show authentication status").action(async function authStatusAction() {
827
+ const { store } = await resolveGlobalOptions(this);
828
+ const apiKey = await store.readApiKey();
829
+ const fromEnv = trimString2(process.env.STARCITE_API_KEY);
830
+ if (fromEnv) {
831
+ logger.info("Authenticated via STARCITE_API_KEY.");
832
+ return;
833
+ }
834
+ if (apiKey) {
835
+ logger.info("Authenticated via saved API key.");
836
+ return;
837
+ }
838
+ logger.info("No API key configured. Run `starcite auth login`.");
839
+ })
840
+ );
841
+ program.command("sessions").description("Manage sessions").addCommand(
842
+ new Command("list").description("List sessions").option(
843
+ "--limit <count>",
844
+ "Maximum sessions to return",
845
+ (value) => parsePositiveInteger(value, "--limit")
846
+ ).option("--cursor <cursor>", "Pagination cursor").option("--metadata <json>", "Metadata filter JSON object").action(async function sessionsListAction(options) {
847
+ const { baseUrl, apiKey, json } = await resolveGlobalOptions(this);
848
+ const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
849
+ const metadata = options.metadata ? parseSessionMetadataFilters(options.metadata) : void 0;
850
+ const cursor = options.cursor?.trim();
851
+ if (options.cursor !== void 0 && !cursor) {
852
+ throw new InvalidArgumentError("--cursor must be non-empty");
853
+ }
854
+ const page = await client.listSessions({
855
+ limit: options.limit,
856
+ cursor,
857
+ metadata
858
+ });
859
+ if (json) {
860
+ logger.info(JSON.stringify(page, null, 2));
861
+ return;
862
+ }
863
+ if (page.sessions.length === 0) {
864
+ logger.info("No sessions found.");
865
+ return;
866
+ }
867
+ logger.info("id title created_at");
868
+ for (const session of page.sessions) {
869
+ logger.info(
870
+ `${session.id} ${session.title ?? ""} ${session.created_at}`
871
+ );
872
+ }
873
+ if (page.next_cursor) {
874
+ logger.info(`next_cursor=${page.next_cursor}`);
875
+ }
876
+ })
877
+ );
878
+ program.command("up").description("Start local Starcite services with Docker").option("-y, --yes", "Skip confirmation prompts and use defaults").option(
879
+ "--port <port>",
880
+ "Starcite API port",
881
+ (value) => parsePort2(value, "--port")
882
+ ).option(
883
+ "--db-port <port>",
884
+ "Postgres port",
885
+ (value) => parsePort2(value, "--db-port")
886
+ ).option("--image <image>", "Override Starcite image").action(async function upAction(options) {
887
+ const { baseUrl, store } = await resolveGlobalOptions(this);
888
+ await runUpWizard({
889
+ baseUrl,
890
+ logger,
891
+ options,
892
+ prompt,
893
+ runCommand,
894
+ store
895
+ });
896
+ });
897
+ program.command("down").description("Stop and remove local Starcite services").option("-y, --yes", "Skip confirmation prompt").option("--no-volumes", "Keep Postgres volume data").action(async function downAction(options) {
898
+ const { store } = await resolveGlobalOptions(this);
899
+ await runDownWizard({
900
+ logger,
901
+ options,
902
+ prompt,
903
+ runCommand,
904
+ store
905
+ });
906
+ });
907
+ program.command("create").description("Create a session").option("--id <id>", "Session ID").option("--title <title>", "Session title").option("--metadata <json>", "Session metadata JSON object").action(async function createAction(options) {
908
+ const { baseUrl, apiKey, json } = await resolveGlobalOptions(this);
909
+ const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
910
+ const metadata = options.metadata ? parseJsonObject(options.metadata, "--metadata") : void 0;
911
+ const session = await client.create({
912
+ id: options.id,
913
+ title: options.title,
914
+ metadata
915
+ });
916
+ if (json) {
917
+ logger.info(
918
+ JSON.stringify(session.record ?? { id: session.id }, null, 2)
919
+ );
920
+ return;
921
+ }
922
+ logger.info(session.id);
923
+ });
924
+ program.command("append <sessionId>").description("Append an event").option("--agent <agent>", "Agent name (high-level mode)").option("--text <text>", "Text content (high-level mode)").option("--type <type>", "Event type", "content").option("--source <source>", "Event source").option(
925
+ "--producer-id <id>",
926
+ "Producer identity (auto-generated if omitted)"
927
+ ).option(
928
+ "--producer-seq <seq>",
929
+ "Producer sequence (defaults to persisted state, starting at 1)",
930
+ (value) => parsePositiveInteger(value, "--producer-seq")
931
+ ).option("--actor <actor>", "Raw actor field (raw mode)").option("--payload <json>", "Raw payload JSON object (raw mode)").option("--metadata <json>", "Event metadata JSON object").option("--refs <json>", "Event refs JSON object").option("--idempotency-key <key>", "Idempotency key").option(
932
+ "--expected-seq <seq>",
933
+ "Expected sequence",
934
+ (value) => parseNonNegativeInteger(value, "--expected-seq")
935
+ ).action(async function appendAction(sessionId, options) {
936
+ const { baseUrl, apiKey, json, store } = await resolveGlobalOptions(this);
937
+ const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
938
+ const session = client.session(sessionId);
939
+ const metadata = options.metadata ? parseJsonObject(options.metadata, "--metadata") : void 0;
940
+ const refs = options.refs ? parseEventRefs(options.refs) : void 0;
941
+ const highLevelMode = options.agent !== void 0 || options.text !== void 0;
942
+ const rawMode = options.actor !== void 0 || options.payload !== void 0;
943
+ if (highLevelMode && rawMode) {
944
+ throw new Error(
945
+ "Choose either high-level mode (--agent and --text) or raw mode (--actor and --payload), not both"
946
+ );
947
+ }
948
+ const producerId = await store.resolveProducerId(options.producerId);
949
+ const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
950
+ const contextKey = buildSeqContextKey(
951
+ normalizedBaseUrl,
952
+ sessionId,
953
+ producerId
954
+ );
955
+ const response = await store.withStateLock(async () => {
956
+ const producerSeq = options.producerSeq ?? await store.readNextSeq(contextKey);
957
+ const appendOptions = {
958
+ ...options,
959
+ producerId,
960
+ producerSeq
961
+ };
962
+ const appendResponse = highLevelMode ? await appendHighLevel(session, appendOptions, metadata, refs) : await appendRaw(session, appendOptions, metadata, refs);
963
+ await store.bumpNextSeq(contextKey, producerSeq);
964
+ return appendResponse;
965
+ });
966
+ if (json) {
967
+ logger.info(JSON.stringify(response, null, 2));
968
+ return;
969
+ }
970
+ logger.info(
971
+ `seq=${response.seq} last_seq=${response.last_seq} deduped=${response.deduped}`
972
+ );
973
+ });
974
+ program.command("tail <sessionId>").description("Tail events from a session").option(
975
+ "--cursor <cursor>",
976
+ "Replay cursor",
977
+ (value) => parseNonNegativeInteger(value, "--cursor")
978
+ ).option("--agent <agent>", "Filter by agent name").option(
979
+ "--limit <count>",
980
+ "Stop after N events",
981
+ (value) => parseNonNegativeInteger(value, "--limit")
982
+ ).action(async function tailAction(sessionId, options) {
983
+ const { baseUrl, apiKey, json } = await resolveGlobalOptions(this);
984
+ const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
985
+ const session = client.session(sessionId);
986
+ const abortController = new AbortController();
987
+ const onSigint = () => {
988
+ abortController.abort();
989
+ };
990
+ process.once("SIGINT", onSigint);
991
+ try {
992
+ let emitted = 0;
993
+ for await (const event of session.tail({
994
+ cursor: options.cursor ?? 0,
995
+ agent: options.agent,
996
+ signal: abortController.signal
997
+ })) {
998
+ if (json) {
999
+ logger.info(JSON.stringify(event));
1000
+ } else {
1001
+ logger.info(formatTailEvent(event));
1002
+ }
1003
+ emitted += 1;
1004
+ if (options.limit !== void 0 && emitted >= options.limit) {
1005
+ abortController.abort();
1006
+ break;
1007
+ }
1008
+ }
1009
+ } finally {
1010
+ process.removeListener("SIGINT", onSigint);
1011
+ }
1012
+ });
1013
+ return program;
1014
+ }
1015
+ function appendHighLevel(session, options, metadata, refs) {
1016
+ if (!(options.agent && options.text)) {
1017
+ throw new Error(
1018
+ "--agent and --text are required for high-level append mode"
1019
+ );
1020
+ }
1021
+ return session.append({
1022
+ agent: options.agent,
1023
+ producerId: options.producerId,
1024
+ producerSeq: options.producerSeq,
1025
+ text: options.text,
1026
+ type: options.type,
1027
+ source: options.source,
1028
+ metadata,
1029
+ refs,
1030
+ idempotencyKey: options.idempotencyKey,
1031
+ expectedSeq: options.expectedSeq
1032
+ });
1033
+ }
1034
+ function appendRaw(session, options, metadata, refs) {
1035
+ if (!(options.actor && options.payload)) {
1036
+ throw new Error(
1037
+ "Raw append mode requires --actor and --payload, or use --agent and --text"
1038
+ );
1039
+ }
1040
+ return session.appendRaw({
1041
+ type: options.type,
1042
+ payload: parseJsonObject(options.payload, "--payload"),
1043
+ actor: options.actor,
1044
+ producer_id: options.producerId,
1045
+ producer_seq: options.producerSeq,
1046
+ source: options.source,
1047
+ metadata,
1048
+ refs,
1049
+ idempotency_key: options.idempotencyKey,
1050
+ expected_seq: options.expectedSeq
1051
+ });
1052
+ }
1053
+ async function run(argv = process.argv, deps = {}) {
1054
+ const program = buildProgram(deps);
1055
+ try {
1056
+ await program.parseAsync(argv);
1057
+ } catch (error) {
1058
+ if (error instanceof StarciteApiError) {
1059
+ const logger2 = deps.logger ?? defaultLogger;
1060
+ logger2.error(`${error.code} (${error.status}): ${error.message}`);
1061
+ process.exitCode = 1;
1062
+ return;
1063
+ }
1064
+ if (error instanceof Error) {
1065
+ const logger2 = deps.logger ?? defaultLogger;
1066
+ logger2.error(error.message);
1067
+ process.exitCode = 1;
1068
+ return;
1069
+ }
1070
+ const logger = deps.logger ?? defaultLogger;
1071
+ logger.error("Unknown error");
1072
+ process.exitCode = 1;
1073
+ }
1074
+ }
1075
+
1076
+ // src/index.ts
1077
+ run();
1078
+ //# sourceMappingURL=index.js.map