starcite 0.0.6 → 0.0.8

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