starcite 0.0.3 → 0.0.5

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
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import {
5
- createStarciteClient,
6
- normalizeBaseUrl,
7
- StarciteApiError
5
+ Starcite,
6
+ StarciteApiError,
7
+ StarciteIdentity
8
8
  } from "@starcite/sdk";
9
9
  import { Command, InvalidArgumentError } from "commander";
10
10
  import { createConsola } from "consola";
@@ -13,7 +13,7 @@ import { z as z2 } from "zod";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "starcite",
16
- version: "0.0.3",
16
+ version: "0.0.5",
17
17
  description: "CLI for Starcite",
18
18
  license: "Apache-2.0",
19
19
  homepage: "https://starcite.ai",
@@ -45,6 +45,7 @@ var package_default = {
45
45
  dev: "bun run src/index.ts",
46
46
  compile: "bun run --cwd ../typescript-sdk build && bun build --compile src/index.ts --outfile dist/starcite",
47
47
  test: "vitest run && bun run test:dist",
48
+ "test:live": "vitest run test/live.api.integration.test.ts",
48
49
  typecheck: "tsc -p tsconfig.json --noEmit",
49
50
  prepublishOnly: "bun run clean && bun run build",
50
51
  "publish:dry": "bun publish --dry-run --access public",
@@ -55,7 +56,7 @@ var package_default = {
55
56
  },
56
57
  dependencies: {
57
58
  "@clack/prompts": "^1.0.1",
58
- "@starcite/sdk": "^0.0.2",
59
+ "@starcite/sdk": "^0.0.5",
59
60
  commander: "^13.1.0",
60
61
  conf: "^15.1.0",
61
62
  consola: "^3.4.2",
@@ -630,7 +631,15 @@ var cliVersion = package_default.version;
630
631
  var nonNegativeIntegerSchema = z2.coerce.number().int().nonnegative();
631
632
  var positiveIntegerSchema = z2.coerce.number().int().positive();
632
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
+ });
633
640
  var TRAILING_SLASHES_REGEX = /\/+$/;
641
+ var DEFAULT_TAIL_BATCH_SIZE = 256;
642
+ var DEFAULT_CREATE_AGENT_ID = "starcite-cli";
634
643
  function parseNonNegativeInteger(value, optionName) {
635
644
  const parsed = nonNegativeIntegerSchema.safeParse(value);
636
645
  if (!parsed.success) {
@@ -683,27 +692,19 @@ function parseConfigSetKey(value) {
683
692
  "config key must be one of: endpoint, producer-id, api-key"
684
693
  );
685
694
  }
686
- function parseJsonOption(value, schema, optionName, invalidShapeMessage) {
695
+ function parseJsonObject(value, optionName) {
687
696
  let parsed;
688
697
  try {
689
698
  parsed = JSON.parse(value);
690
699
  } catch {
691
700
  throw new InvalidArgumentError(`${optionName} must be valid JSON`);
692
701
  }
693
- const result = schema.safeParse(parsed);
702
+ const result = jsonObjectSchema.safeParse(parsed);
694
703
  if (!result.success) {
695
- throw new InvalidArgumentError(
696
- `${optionName} must be ${invalidShapeMessage}`
697
- );
704
+ throw new InvalidArgumentError(`${optionName} must be a JSON object`);
698
705
  }
699
706
  return result.data;
700
707
  }
701
- function parseJsonObject(value, optionName) {
702
- return parseJsonOption(value, jsonObjectSchema, optionName, "a JSON object");
703
- }
704
- function parseEventRefs(value) {
705
- return parseJsonObject(value, "--refs");
706
- }
707
708
  function parseSessionMetadataFilters(value) {
708
709
  const parsed = parseJsonObject(value, "--metadata");
709
710
  const filters = {};
@@ -719,7 +720,12 @@ function parseSessionMetadataFilters(value) {
719
720
  return filters;
720
721
  }
721
722
  function getGlobalOptions(command) {
722
- return command.optsWithGlobals();
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;
723
729
  }
724
730
  function trimString2(value) {
725
731
  const trimmed = value?.trim();
@@ -742,403 +748,439 @@ async function resolveGlobalOptions(command) {
742
748
  store
743
749
  };
744
750
  }
745
- async function promptForEndpoint(prompt, defaultEndpoint) {
746
- while (true) {
747
- const answer = await prompt.input("Starcite endpoint URL", defaultEndpoint);
748
- try {
749
- return parseEndpoint(answer, "endpoint");
750
- } catch {
751
- }
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}`;
752
756
  }
757
+ return `[${actorLabel}] ${JSON.stringify(event.payload)}`;
753
758
  }
754
- async function promptForApiKey(prompt) {
755
- const message = "Paste your Starcite API key";
756
- const answer = prompt.password ? await prompt.password(message) : await prompt.input(message, "");
757
- return trimString2(answer) ?? "";
759
+ function parseJwtClaims(token) {
760
+ const parts = token.split(".");
761
+ if (parts.length < 2) {
762
+ return void 0;
763
+ }
764
+ const payload = parts[1];
765
+ if (!payload) {
766
+ return void 0;
767
+ }
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;
774
+ }
758
775
  }
759
- function formatTailEvent(event) {
760
- const actorLabel = event.agent ?? event.actor;
761
- if (event.text) {
762
- return `[${actorLabel}] ${event.text}`;
776
+ function tokenTenantId(token) {
777
+ if (!token) {
778
+ return void 0;
763
779
  }
764
- return `[${actorLabel}] ${JSON.stringify(event.payload)}`;
780
+ const claims = parseJwtClaims(token);
781
+ const tenantId = claims?.tenant_id;
782
+ if (typeof tenantId !== "string") {
783
+ return void 0;
784
+ }
785
+ const trimmed = tenantId.trim();
786
+ return trimmed.length > 0 ? trimmed : void 0;
765
787
  }
766
- function buildProgram(deps = {}) {
767
- const createClient = deps.createClient ?? ((baseUrl, apiKey) => createStarciteClient({
768
- baseUrl,
769
- apiKey
770
- }));
771
- const logger = deps.logger ?? defaultLogger;
772
- const prompt = deps.prompt ?? createDefaultPrompt();
773
- const runCommand = deps.runCommand ?? defaultCommandRunner;
774
- const program = new Command();
775
- 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 / service JWT").option(
776
- "--config-dir <path>",
777
- "Starcite CLI config directory (default: ~/.starcite)"
778
- ).option("--json", "Output JSON");
779
- program.command("version").description("Print current CLI version").action(() => {
780
- logger.info(cliVersion);
781
- });
782
- 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) {
783
- const { baseUrl, json, store } = await resolveGlobalOptions(this);
784
- const defaultEndpoint = parseEndpoint(baseUrl, "endpoint");
785
- let endpoint = defaultEndpoint;
786
- if (options.endpoint) {
787
- endpoint = parseEndpoint(options.endpoint, "--endpoint");
788
- } else if (!options.yes) {
789
- endpoint = await promptForEndpoint(prompt, defaultEndpoint);
790
- }
791
- await store.updateConfig({ baseUrl: endpoint });
792
- let apiKey = trimString2(options.apiKey);
793
- if (!(apiKey || options.yes)) {
794
- apiKey = await promptForApiKey(prompt);
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
+ }
795
800
  }
796
- if (apiKey) {
797
- await store.saveApiKey(apiKey);
798
- await store.updateConfig({ apiKey: void 0 });
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
+ }
799
808
  }
800
- if (json) {
801
- logger.info(
802
- JSON.stringify(
803
- {
804
- configDir: store.directory,
805
- endpoint,
806
- apiKeySaved: Boolean(apiKey)
807
- },
808
- null,
809
- 2
810
- )
809
+ }
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"
811
830
  );
812
- return;
813
831
  }
814
- logger.info(`Initialized Starcite CLI in ${store.directory}`);
815
- logger.info(`Endpoint set to ${endpoint}`);
816
- if (apiKey) {
817
- logger.info("API key saved. You can now run create/append/tail.");
818
- } else {
819
- logger.info("API key not set. Run `starcite auth login` when ready.");
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
+ );
820
841
  }
842
+ return { kind: "raw", actor, payload };
843
+ }
844
+ throw new InvalidArgumentError(
845
+ "append requires either high-level mode (--agent and --text) or raw mode (--actor and --payload)"
846
+ );
847
+ }
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://");
852
+ }
853
+ const normalized = parsed.toString().replace(TRAILING_SLASHES_REGEX, "");
854
+ return normalized.endsWith("/v1") ? normalized : `${normalized}/v1`;
855
+ }
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
+ );
861
+ }
862
+ if (shouldAutoIssueSessionToken(apiKey)) {
863
+ return await client.session({
864
+ identity: resolveCreateIdentity(apiKey),
865
+ id: sessionId
866
+ });
867
+ }
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}'`
872
+ );
873
+ }
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
+ );
882
+ }
883
+ return new StarciteIdentity({
884
+ tenantId,
885
+ id: agentId,
886
+ type: "agent"
821
887
  });
822
- program.command("config").description("Manage CLI configuration").addCommand(
823
- 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) {
824
- const { store } = await resolveGlobalOptions(this);
825
- const parsedKey = parseConfigSetKey(key);
826
- if (parsedKey === "endpoint") {
827
- const endpoint = parseEndpoint(value, "endpoint");
828
- await store.updateConfig({ baseUrl: endpoint });
829
- logger.info(`Endpoint set to ${endpoint}`);
830
- return;
831
- }
832
- if (parsedKey === "producer-id") {
833
- const producerId = trimString2(value);
834
- if (!producerId) {
835
- throw new InvalidArgumentError("producer-id cannot be empty");
888
+ }
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;
836
925
  }
837
- await store.updateConfig({ producerId });
838
- logger.info(`Producer ID set to ${producerId}`);
839
- return;
840
- }
841
- await store.saveApiKey(value);
842
- await store.updateConfig({ apiKey: void 0 });
843
- logger.info("API key saved.");
844
- })
845
- ).addCommand(
846
- new Command("show").description("Show current configuration").action(async function configShowAction() {
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,
982
+ cursor,
983
+ metadata
984
+ });
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}`);
1001
+ }
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) {
847
1013
  const { baseUrl, store } = await resolveGlobalOptions(this);
848
- const config = await store.readConfig();
849
- const apiKey = await store.readApiKey();
850
- const fromEnv = trimString2(process.env.STARCITE_API_KEY);
851
- let apiKeySource = "unset";
852
- if (fromEnv) {
853
- apiKeySource = "env";
854
- } else if (apiKey) {
855
- apiKeySource = "stored";
856
- }
857
- logger.info(
858
- JSON.stringify(
859
- {
860
- endpoint: config.baseUrl ?? baseUrl,
861
- producerId: config.producerId ?? null,
862
- apiKey: apiKey ? "***" : null,
863
- apiKeySource,
864
- configDir: store.directory
865
- },
866
- null,
867
- 2
868
- )
869
- );
870
- })
871
- );
872
- program.command("auth").description("Manage API key authentication").addCommand(
873
- new Command("login").description("Save an API key for authenticated requests").option("--api-key <key>", "API key to store").action(async function authLoginAction(options) {
874
- const { store } = await resolveGlobalOptions(this);
875
- let apiKey = trimString2(options.apiKey);
876
- if (!apiKey) {
877
- apiKey = await promptForApiKey(prompt);
878
- }
879
- if (!apiKey) {
880
- throw new Error("API key cannot be empty");
881
- }
882
- await store.saveApiKey(apiKey);
883
- await store.updateConfig({ apiKey: void 0 });
884
- logger.info("API key saved.");
885
- })
886
- ).addCommand(
887
- new Command("logout").description("Remove the saved API key").action(async function authLogoutAction() {
888
- const { store } = await resolveGlobalOptions(this);
889
- await store.clearApiKey();
890
- logger.info("Saved API key removed.");
891
- })
892
- ).addCommand(
893
- new Command("status").description("Show authentication status").action(async function authStatusAction() {
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) {
894
1024
  const { store } = await resolveGlobalOptions(this);
895
- const apiKey = await store.readApiKey();
896
- const fromEnv = trimString2(process.env.STARCITE_API_KEY);
897
- if (fromEnv) {
898
- logger.info("Authenticated via STARCITE_API_KEY.");
899
- return;
900
- }
901
- if (apiKey) {
902
- logger.info("Authenticated via saved API key.");
903
- return;
904
- }
905
- logger.info("No API key configured. Run `starcite auth login`.");
906
- })
907
- );
908
- program.command("sessions").description("Manage sessions").addCommand(
909
- new Command("list").description("List sessions").option(
910
- "--limit <count>",
911
- "Maximum sessions to return",
912
- (value) => parsePositiveInteger(value, "--limit")
913
- ).option("--cursor <cursor>", "Pagination cursor").option("--metadata <json>", "Metadata filter JSON object").action(async function sessionsListAction(options) {
914
- const { baseUrl, apiKey, json } = await resolveGlobalOptions(this);
915
- const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
916
- const metadata = options.metadata ? parseSessionMetadataFilters(options.metadata) : void 0;
917
- const cursor = options.cursor?.trim();
918
- if (options.cursor !== void 0 && !cursor) {
919
- throw new InvalidArgumentError("--cursor must be non-empty");
920
- }
921
- const page = await client.listSessions({
922
- limit: options.limit,
923
- cursor,
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,
924
1042
  metadata
925
1043
  });
926
1044
  if (json) {
927
- logger.info(JSON.stringify(page, null, 2));
928
- return;
929
- }
930
- if (page.sessions.length === 0) {
931
- logger.info("No sessions found.");
932
- return;
933
- }
934
- logger.info("id title created_at");
935
- for (const session of page.sessions) {
936
1045
  logger.info(
937
- `${session.id} ${session.title ?? ""} ${session.created_at}`
1046
+ JSON.stringify(session.record ?? { id: session.id }, null, 2)
938
1047
  );
1048
+ return;
939
1049
  }
940
- if (page.next_cursor) {
941
- logger.info(`next_cursor=${page.next_cursor}`);
942
- }
943
- })
944
- );
945
- program.command("up").description("Start local Starcite services with Docker").option("-y, --yes", "Skip confirmation prompts and use defaults").option(
946
- "--port <port>",
947
- "Starcite API port",
948
- (value) => parsePort2(value, "--port")
949
- ).option(
950
- "--db-port <port>",
951
- "Postgres port",
952
- (value) => parsePort2(value, "--db-port")
953
- ).option("--image <image>", "Override Starcite image").action(async function upAction(options) {
954
- const { baseUrl, store } = await resolveGlobalOptions(this);
955
- await runUpWizard({
956
- baseUrl,
957
- logger,
958
- options,
959
- prompt,
960
- runCommand,
961
- store
1050
+ logger.info(session.id);
962
1051
  });
963
- });
964
- 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) {
965
- const { store } = await resolveGlobalOptions(this);
966
- await runDownWizard({
967
- logger,
968
- options,
969
- prompt,
970
- runCommand,
971
- store
972
- });
973
- });
974
- 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) {
975
- const { baseUrl, apiKey, json } = await resolveGlobalOptions(this);
976
- const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
977
- const metadata = options.metadata ? parseJsonObject(options.metadata, "--metadata") : void 0;
978
- const session = await client.create({
979
- id: options.id,
980
- title: options.title,
981
- metadata
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));
1109
+ return;
1110
+ }
1111
+ logger.info(`seq=${response.seq} deduped=${response.deduped}`);
982
1112
  });
983
- if (json) {
984
- logger.info(
985
- JSON.stringify(session.record ?? { id: session.id }, null, 2)
986
- );
987
- return;
988
- }
989
- logger.info(session.id);
990
- });
991
- 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(
992
- "--producer-id <id>",
993
- "Producer identity (auto-generated if omitted)"
994
- ).option(
995
- "--producer-seq <seq>",
996
- "Producer sequence (defaults to persisted state, starting at 1)",
997
- (value) => parsePositiveInteger(value, "--producer-seq")
998
- ).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(
999
- "--expected-seq <seq>",
1000
- "Expected sequence",
1001
- (value) => parseNonNegativeInteger(value, "--expected-seq")
1002
- ).action(async function appendAction(sessionId, options) {
1003
- const { baseUrl, apiKey, json, store } = await resolveGlobalOptions(this);
1004
- const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
1005
- const session = client.session(sessionId);
1006
- const metadata = options.metadata ? parseJsonObject(options.metadata, "--metadata") : void 0;
1007
- const refs = options.refs ? parseEventRefs(options.refs) : void 0;
1008
- const highLevelMode = options.agent !== void 0 || options.text !== void 0;
1009
- const rawMode = options.actor !== void 0 || options.payload !== void 0;
1010
- if (highLevelMode && rawMode) {
1011
- throw new Error(
1012
- "Choose either high-level mode (--agent and --text) or raw mode (--actor and --payload), not both"
1013
- );
1014
- }
1015
- const producerId = await store.resolveProducerId(options.producerId);
1016
- const normalizedBaseUrl = normalizeBaseUrl(baseUrl);
1017
- const contextKey = buildSeqContextKey(
1018
- normalizedBaseUrl,
1019
- sessionId,
1020
- producerId
1021
- );
1022
- const response = await store.withStateLock(async () => {
1023
- const producerSeq = options.producerSeq ?? await store.readNextSeq(contextKey);
1024
- const appendOptions = {
1025
- ...options,
1026
- producerId,
1027
- producerSeq
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();
1028
1128
  };
1029
- const appendResponse = highLevelMode ? await appendHighLevel(session, appendOptions, metadata, refs) : await appendRaw(session, appendOptions, metadata, refs);
1030
- await store.bumpNextSeq(contextKey, producerSeq);
1031
- return appendResponse;
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);
1158
+ }
1032
1159
  });
1033
- if (json) {
1034
- logger.info(JSON.stringify(response, null, 2));
1035
- return;
1036
- }
1037
- logger.info(
1038
- `seq=${response.seq} last_seq=${response.last_seq} deduped=${response.deduped}`
1039
- );
1040
- });
1041
- program.command("tail <sessionId>").description("Tail events from a session").option(
1042
- "--cursor <cursor>",
1043
- "Replay cursor",
1044
- (value) => parseNonNegativeInteger(value, "--cursor")
1045
- ).option("--agent <agent>", "Filter by agent name").option(
1046
- "--limit <count>",
1047
- "Stop after N events",
1048
- (value) => parseNonNegativeInteger(value, "--limit")
1049
- ).option("--no-follow", "Exit after replaying stored events").action(async function tailAction(sessionId, options) {
1050
- const { baseUrl, apiKey, json } = await resolveGlobalOptions(this);
1051
- const client = apiKey ? createClient(baseUrl, apiKey) : createClient(baseUrl);
1052
- const session = client.session(sessionId);
1053
- const abortController = new AbortController();
1054
- const onSigint = () => {
1055
- abortController.abort();
1056
- };
1057
- process.once("SIGINT", onSigint);
1160
+ return program;
1161
+ }
1162
+ async run(argv = process.argv) {
1163
+ const program = this.buildProgram();
1058
1164
  try {
1059
- let emitted = 0;
1060
- for await (const event of session.tail({
1061
- cursor: options.cursor ?? 0,
1062
- agent: options.agent,
1063
- follow: options.follow,
1064
- signal: abortController.signal
1065
- })) {
1066
- if (json) {
1067
- logger.info(JSON.stringify(event));
1068
- } else {
1069
- logger.info(formatTailEvent(event));
1070
- }
1071
- emitted += 1;
1072
- if (options.limit !== void 0 && emitted >= options.limit) {
1073
- abortController.abort();
1074
- break;
1075
- }
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;
1170
+ return;
1076
1171
  }
1077
- } finally {
1078
- process.removeListener("SIGINT", onSigint);
1079
- }
1080
- });
1081
- return program;
1082
- }
1083
- function appendHighLevel(session, options, metadata, refs) {
1084
- if (!(options.agent && options.text)) {
1085
- throw new Error(
1086
- "--agent and --text are required for high-level append mode"
1087
- );
1088
- }
1089
- return session.append({
1090
- agent: options.agent,
1091
- producerId: options.producerId,
1092
- producerSeq: options.producerSeq,
1093
- text: options.text,
1094
- type: options.type,
1095
- source: options.source,
1096
- metadata,
1097
- refs,
1098
- idempotencyKey: options.idempotencyKey,
1099
- expectedSeq: options.expectedSeq
1100
- });
1101
- }
1102
- function appendRaw(session, options, metadata, refs) {
1103
- if (!(options.actor && options.payload)) {
1104
- throw new Error(
1105
- "Raw append mode requires --actor and --payload, or use --agent and --text"
1106
- );
1107
- }
1108
- return session.appendRaw({
1109
- type: options.type,
1110
- payload: parseJsonObject(options.payload, "--payload"),
1111
- actor: options.actor,
1112
- producer_id: options.producerId,
1113
- producer_seq: options.producerSeq,
1114
- source: options.source,
1115
- metadata,
1116
- refs,
1117
- idempotency_key: options.idempotencyKey,
1118
- expected_seq: options.expectedSeq
1119
- });
1120
- }
1121
- async function run(argv = process.argv, deps = {}) {
1122
- const program = buildProgram(deps);
1123
- try {
1124
- await program.parseAsync(argv);
1125
- } catch (error) {
1126
- if (error instanceof StarciteApiError) {
1127
- const logger2 = deps.logger ?? defaultLogger;
1128
- logger2.error(`${error.code} (${error.status}): ${error.message}`);
1129
- process.exitCode = 1;
1130
- return;
1131
- }
1132
- if (error instanceof Error) {
1133
- const logger2 = deps.logger ?? defaultLogger;
1134
- logger2.error(error.message);
1172
+ if (error instanceof Error) {
1173
+ this.logger.error(error.message);
1174
+ process.exitCode = 1;
1175
+ return;
1176
+ }
1177
+ this.logger.error("Unknown error");
1135
1178
  process.exitCode = 1;
1136
- return;
1137
1179
  }
1138
- const logger = deps.logger ?? defaultLogger;
1139
- logger.error("Unknown error");
1140
- process.exitCode = 1;
1141
1180
  }
1181
+ };
1182
+ async function run(argv = process.argv, deps = {}) {
1183
+ await new StarciteCliApp(deps).run(argv);
1142
1184
  }
1143
1185
 
1144
1186
  // src/index.ts