ai-hist 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +62 -84
  2. package/dist/cli.js +80 -103
  3. package/dist/cli.js.map +1 -1
  4. package/dist/contracts.d.ts +413 -0
  5. package/dist/contracts.d.ts.map +1 -0
  6. package/dist/contracts.js +2 -0
  7. package/dist/contracts.js.map +1 -0
  8. package/dist/delivery-cli.d.ts +22 -0
  9. package/dist/delivery-cli.d.ts.map +1 -0
  10. package/dist/delivery-cli.js +135 -0
  11. package/dist/delivery-cli.js.map +1 -0
  12. package/dist/delivery-contracts.d.ts +149 -0
  13. package/dist/delivery-contracts.d.ts.map +1 -0
  14. package/dist/delivery-contracts.js +4 -0
  15. package/dist/delivery-contracts.js.map +1 -0
  16. package/dist/delivery-plugins.d.ts +31 -0
  17. package/dist/delivery-plugins.d.ts.map +1 -0
  18. package/dist/delivery-plugins.js +124 -0
  19. package/dist/delivery-plugins.js.map +1 -0
  20. package/dist/delivery.d.ts +57 -0
  21. package/dist/delivery.d.ts.map +1 -0
  22. package/dist/delivery.js +242 -0
  23. package/dist/delivery.js.map +1 -0
  24. package/dist/delivery.test.d.ts +2 -0
  25. package/dist/delivery.test.d.ts.map +1 -0
  26. package/dist/delivery.test.js +396 -0
  27. package/dist/delivery.test.js.map +1 -0
  28. package/dist/git.d.ts +17 -0
  29. package/dist/git.d.ts.map +1 -0
  30. package/dist/git.js +13 -0
  31. package/dist/git.js.map +1 -0
  32. package/dist/history-export.d.ts +30 -0
  33. package/dist/history-export.d.ts.map +1 -0
  34. package/dist/history-export.js +42 -0
  35. package/dist/history-export.js.map +1 -0
  36. package/dist/index.d.ts +5 -533
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +5 -833
  39. package/dist/index.js.map +1 -1
  40. package/dist/mcp-server.js +31 -30
  41. package/dist/mcp-server.js.map +1 -1
  42. package/dist/native.d.ts +5 -15
  43. package/dist/native.d.ts.map +1 -1
  44. package/dist/native.js +1 -1
  45. package/dist/native.js.map +1 -1
  46. package/dist/normalization.d.ts +65 -0
  47. package/dist/normalization.d.ts.map +1 -0
  48. package/dist/normalization.js +354 -0
  49. package/dist/normalization.js.map +1 -0
  50. package/dist/operations.d.ts +101 -0
  51. package/dist/operations.d.ts.map +1 -0
  52. package/dist/operations.js +677 -0
  53. package/dist/operations.js.map +1 -0
  54. package/dist/pagination.d.ts +25 -0
  55. package/dist/pagination.d.ts.map +1 -0
  56. package/dist/pagination.js +162 -0
  57. package/dist/pagination.js.map +1 -0
  58. package/dist/source-contracts.d.ts +63 -0
  59. package/dist/source-contracts.d.ts.map +1 -0
  60. package/dist/source-contracts.js +2 -0
  61. package/dist/source-contracts.js.map +1 -0
  62. package/dist/source-plugins.d.ts +35 -0
  63. package/dist/source-plugins.d.ts.map +1 -0
  64. package/dist/source-plugins.js +177 -0
  65. package/dist/source-plugins.js.map +1 -0
  66. package/dist/source-plugins.test.d.ts +2 -0
  67. package/dist/source-plugins.test.d.ts.map +1 -0
  68. package/dist/source-plugins.test.js +379 -0
  69. package/dist/source-plugins.test.js.map +1 -0
  70. package/package.json +17 -17
  71. package/dist/cloud-auth-bundle.d.ts +0 -2
  72. package/dist/cloud-auth-bundle.d.ts.map +0 -1
  73. package/dist/cloud-auth-bundle.js +0 -1112
  74. package/dist/cloud-auth-bundle.js.map +0 -7
  75. package/dist/cloud-client.d.ts +0 -221
  76. package/dist/cloud-client.d.ts.map +0 -1
  77. package/dist/cloud-client.js +0 -283
  78. package/dist/cloud-client.js.map +0 -1
  79. package/dist/cloud-preflight.d.ts +0 -29
  80. package/dist/cloud-preflight.d.ts.map +0 -1
  81. package/dist/cloud-preflight.js +0 -72
  82. package/dist/cloud-preflight.js.map +0 -1
package/README.md CHANGED
@@ -1,8 +1,9 @@
1
1
  # ai-hist
2
2
 
3
- The public TypeScript SDK, Node CLI, and MCP server for RelayHistory. Every
4
- operation uses the mandatory `ai-hist-native` Node-API engine; there is no
5
- JavaScript SQLite implementation or provider-file fallback.
3
+ The local history TypeScript SDK, Node CLI, and MCP server. Storage and local
4
+ ingestion use one mandatory `ai-hist-native` Node-API engine. Optional source and
5
+ destination plugins compose through public interfaces; no JavaScript SQL or
6
+ commercial dependency is required by this package.
6
7
 
7
8
  ```bash
8
9
  npm install ai-hist
@@ -50,7 +51,8 @@ cache-only. `discoverSessions` is shallow discovery. `hydrateSession` is
50
51
  targeted evidence acquisition for one existing catalog row. It returns
51
52
  `hydrated`, `updated`, `unchanged`, or `capability_limited`, an indexed source
52
53
  stamp, evidence counts, related native session IDs, and bounded-work metrics.
53
- Targeted remote hydration uses `scope: 'remote'`: Claude web sessions can
54
+ Targeted remote hydration uses `scope: 'remote'` and an explicitly configured
55
+ source plugin registry: Claude web sessions can
54
56
  return `capability: 'full'` after complete teleport evidence is acquired;
55
57
  Codex cloud tasks return `partial` when their supported unified diff is indexed
56
58
  or `shallow_only` when no richer evidence is exposed. Partial results retain
@@ -70,14 +72,29 @@ offline behavior and making provider-cloud access explicit. The CLI exposes the
70
72
  same mutually exclusive `--local`, `--remote`, and `--all` flags; omitting them
71
73
  is equivalent to `--local`.
72
74
 
73
- Cached reads already support every scope. Remote acquisition runs through
74
- provider connectors claude.ai/code web sessions and Codex cloud tasks
75
- that are configured by the provider CLI's own sign-in on the machine (see the
76
- repository's `docs/remote-connectors.md`). On a machine with no connector
77
- configured, `discoverSessions({ scope: 'remote' })` and
78
- `sync({ scope: 'remote' })` fail with `UnsupportedOperationError` and the stable
79
- code `UNSUPPORTED_OPERATION`; they never fall back to local acquisition.
80
- `scope: 'all'` runs the local adapters plus every configured connector.
75
+ Cached reads preserve the requested scope and never consult commercial auth.
76
+ Stored remote history can be queried with absent, malformed, expired, or
77
+ ambiguous credentials. Remote acquisition requires an explicitly loaded plugin
78
+ registry. Install `@agent-relay/history-provider-sources` for Claude web/Codex
79
+ cloud, or `@agent-relay/relayhistory` for RelayHistory. Installing a package does
80
+ not register it, inspect auth or start delivery.
81
+
82
+ ```ts
83
+ import { HistoryPluginRegistry } from 'ai-hist';
84
+ import { createHistoryPlugin } from '@agent-relay/history-provider-sources';
85
+ const plugins = new HistoryPluginRegistry();
86
+ plugins.register(createHistoryPlugin({connectors:['claude-web']}));
87
+ await discoverSessions({ scope:'remote', plugins, sourceConnectors:['claude-web'] });
88
+ await sync({ scope:'all', plugins, sourceConnectors:[] }); // local adapters only
89
+ const cached = await stats({ scope:'remote' }); // no login required
90
+ ```
91
+
92
+ The CLI accepts `--config history.json` on acquisition commands. Config lists
93
+ explicit plugin modules/options; `--source-connector` selects their IDs and is
94
+ repeatable. `--no-source-connectors` disables remote acquisition. MCP uses
95
+ `AI_HIST_PLUGIN_CONFIG` and `source_connectors`. Scope defaults to local even
96
+ with plugins configured. See [source plugins](../docs/remote-connectors.md).
97
+ Native contract 14 rejects old addons that lack the source/delivery boundary.
81
98
 
82
99
  Catalog pages, discovery results, statistics, and sync results echo the requested `scope`,
83
100
  and discovery results additionally report `locationsRun` — the connector
@@ -188,75 +205,36 @@ evidence, and connector/parser failures with dedicated error subclasses.
188
205
  The old synchronous `AiHist` class and `openAiHist()` API were removed in 1.0.
189
206
  See [the migration guide](https://github.com/AgentWorkforce/relayhistory/blob/main/docs/native-sdk-migration.md).
190
207
 
191
- ## Cloud opt-in
192
-
193
- Run `ai-hist enable-cloud` to log in, drain local history and keep pushing. Use `--once` to exit after draining. The npm CLI bundles Agent Relay Cloud login, so no separate `agent-relay` CLI install is required; a run without a TTY fails promptly with interactive-login and token guidance. The async SDK exports `enableCloud`, `pushCloud`, `installGitHooks` and `createShareableTrace`; RelayHistory exchange, transport, and stage-scoped auth stay in Rust. See [cloud setup](https://github.com/AgentWorkforce/relayhistory/blob/main/docs/enable-cloud.md).
194
-
195
- `ai-hist/cloud` owns the cloud API wrappers, including `loginCloud` and
196
- `loadStoredRelayhistoryAuth`, and delegates to the Rust cloud layer through N-API.
197
- The root `ai-hist` entrypoint re-exports the cloud API for convenience. Credentials
198
- live in `$RELAYHISTORY_HOME/stages`, defaulting to
199
- `~/.agentworkforce/relayhistory/stages`. The Rust layer selects the stage, checks
200
- transport security, preserves session metadata, and saves rotated tokens atomically.
201
-
202
- ## Session thread transport
203
-
204
- `getSessionThread` accepts `SessionThreadOptions.fetchImpl` for the thread GET
205
- request and its retries. Authentication requests use the Rust HTTP client.
206
- A stored-session 401 can therefore trigger a native refresh request even when
207
- `fetchImpl` is mocked. Custom proxy or TLS settings supplied through `fetchImpl`
208
- apply only to thread requests; the auth endpoint must also be reachable by the
209
- native client for automatic refresh to succeed.
210
-
211
- For isolated mocks or caller-managed credentials, supply `resolveSession` that
212
- returns `{ auth }` without the `session: true` marker:
213
-
214
- ```ts
215
- import { getSessionThread } from 'ai-hist/cloud';
216
-
217
- const thread = await getSessionThread(
218
- { source: 'claude', sessionId: 'example-session' },
219
- {
220
- resolveSession: async () => ({
221
- auth: { baseUrl: 'https://history.example.com', accessToken: 'rth_at_fixture' },
222
- }),
223
- fetchImpl: async () => new Response(JSON.stringify({
224
- session: null, outcomes: [], links: [], nextCursor: null,
225
- })),
226
- },
227
- );
228
- ```
229
-
230
- This resolver disables automatic refresh. A 401 from the selected transport
231
- throws `AuthenticationExpiredError` without an authentication request. The
232
- default `resolveCloudSession` returns the marker for stored credentials and
233
- enables native refresh.
234
-
235
- ## Cloud token and replay
236
-
237
- The npm CLI uses the same Rust engine as the public async SDK:
238
-
239
- ```bash
240
- # Shell-safe token export (fails if ai-hist token fails)
241
- RTH_TOKEN="$(ai-hist token)" || { echo "Failed to get token" >&2; exit 1; }
242
- export RTH_TOKEN
243
- ai-hist replay SESSION_ID
244
- ai-hist replay SESSION_ID --json --out transcript.json
245
- ```
246
-
247
- ```ts
248
- import { accessToken, replay } from 'ai-hist';
249
-
250
- const token = await accessToken(); // Secret: do not log it.
251
- const result = await replay('SESSION_ID', { json: true });
252
- const events = JSON.parse(result.transcript!);
253
- await replay('SESSION_ID', { json: true, out: 'transcript.json' });
254
- ```
255
-
256
- Both APIs accept `baseUrl` (`--base-url` in the CLI) for explicit stage selection.
257
- Token refresh and persistence run in Rust before returning a token with at least
258
- 60 seconds of recorded validity. Piped `token` stdout contains only the token
259
- and one newline; failures leave stdout empty. Replay fetches every page in
260
- server order; `limit` is the page size, and `maxContent` caps each event's content.
261
- File output replaces its destination atomically only after all pages succeed.
262
- Neither command opens or imports into the local history database.
208
+ ## Optional cloud services
209
+
210
+ Use a `HistoryDestination` plugin for any service or pipe the public NDJSON
211
+ export to your own program. The local package has no cloud exports, login CLI,
212
+ or default cloud MCP tool. RelayHistory's auth, sharing, replay, durable upload
213
+ and readback live in [`@agent-relay/relayhistory`](../plugins/relayhistory/sdk/README.md).
214
+ Move imports from `ai-hist/cloud` to that package. Git hooks and commit linking
215
+ remain local SDK operations.
216
+
217
+ ## Export and durable delivery
218
+
219
+ Use `exportHistory(selection)` for a bounded historical snapshot or
220
+ `ai-hist export --selection selection.json` for NDJSON stdout. Explicitly enabled
221
+ delivery jobs use `createHistoryDelivery`, `HistoryPluginRegistry`, and
222
+ `drainHistoryDelivery`/`runHistoryDelivery`. The same Rust queue handles one-shot
223
+ and background runs, immutable retries, exact acknowledgments, and worker leases.
224
+ Native contract 14 is required. See [delivery setup and contracts](../docs/history-delivery.md).
225
+
226
+ Source discovery and hydration accept `acquisitionTimeoutMs` for each selected
227
+ connector operation, including a complete paginated snapshot. The default is
228
+ 300,000 ms; choose an integer from 1 through 3,600,000 ms. CLI acquisitions accept
229
+ `--acquisition-timeout-ms N`; MCP acquisitions accept `acquisition_timeout_ms`.
230
+ The same budget and cancellation signal reach the optional source helper.
231
+ Timeouts return `SOURCE_ACQUISITION_TIMEOUT`, cancellation returns
232
+ `SOURCE_ACQUISITION_CANCELLED`, and neither commits a partial snapshot. Typed
233
+ source failures such as `AUTHENTICATION_EXPIRED` and `SESSION_NOT_FOUND` retain
234
+ their public classes/codes with sanitized messages.
235
+
236
+ To remove a persistent delivery exclusion, cancel affected delivery jobs first,
237
+ clear the exclusion, then create new jobs to backfill the skipped history. A
238
+ running or paused generation cannot rewind revisions it already skipped;
239
+ attempting this returns `DELIVERY_GENERATION_REQUIRED`. Jobs whose selection
240
+ permanently excludes that session or cannot include it may continue.
package/dist/cli.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { readFile } from 'node:fs/promises';
3
- import { discoverSessions, ensureLocalStore, formatSessionRow, getSession, getSessionEventsPage, getSessionFileEditsPage, getSessionRelationships, getSessionToolCallsPage, getSessionTree, hydrateSession, listSessionCatalogPage, loadStoredRelayhistoryAuth, login, recent, resumeCommand, search, stats, sync, enableCloud, accessToken, replay, validateCloudExchangeBaseUrl, } from './index.js';
4
- import { prepareCloudSessionForEnableCloud } from './cloud-preflight.js';
5
- const BOOLEAN_FLAGS = new Set(['all', 'fts', 'help', 'json', 'local', 'no-bootstrap', 'no-related', 'no-warning', 'once', 'pretty', 'remote', 'version']);
3
+ import { discoverSessions, ensureLocalStore, formatSessionRow, getSession, getSessionEventsPage, getSessionFileEditsPage, getSessionRelationships, getSessionToolCallsPage, getSessionTree, hydrateSession, listSessionCatalogPage, recent, resumeCommand, search, stats, sync, } from './index.js';
4
+ import { runDeliveryCommand, runHistoryExportCommand, loadHistoryApplicationConfig } from './delivery-cli.js';
5
+ const BOOLEAN_FLAGS = new Set(['all', 'fts', 'help', 'json', 'local', 'no-bootstrap', 'no-related', 'no-source-connectors', 'no-warning', 'once', 'pretty', 'remote', 'version']);
6
6
  const VALUE_FLAGS = new Set([
7
- 'base-url', 'interval', 'label', 'max-content', 'out', 'after', 'after-ms', 'after-session-id', 'after-source', 'before-ms', 'db', 'limit',
8
- 'max-depth', 'max-nodes', 'project', 'source', 'tag', 'token', 'tokens',
7
+ 'config', 'job', 'selection', 'poll-ms', 'timeout-ms', 'base-url', 'interval', 'label', 'max-content', 'out', 'after', 'after-ms', 'after-session-id', 'after-source', 'before-ms', 'db', 'limit',
8
+ 'max-depth', 'max-nodes', 'config', 'source-connector', 'project', 'source', 'tag', 'token', 'tokens',
9
9
  ]);
10
10
  const KNOWN_FLAGS = new Set([...BOOLEAN_FLAGS, ...VALUE_FLAGS]);
11
11
  function versionTriple(value) {
@@ -101,21 +101,17 @@ function textFlag(args, name) {
101
101
  const value = args.flags.get(name)?.at(-1);
102
102
  return typeof value === 'string' ? value : undefined;
103
103
  }
104
- async function relayAccessTokenForCloudCommand(command, rawArgs, args, reuseStoredAuth) {
105
- const explicitToken = textFlag(args, 'token');
106
- if (explicitToken !== undefined)
107
- return explicitToken;
108
- const baseUrl = textFlag(args, 'base-url');
109
- if (reuseStoredAuth && await loadStoredRelayhistoryAuth(baseUrl))
110
- return undefined;
111
- const preparedToken = await prepareCloudSessionForEnableCloud(command, rawArgs, process.env);
112
- if (preparedToken !== null)
113
- await validateCloudExchangeBaseUrl(baseUrl);
114
- return preparedToken ?? undefined;
115
- }
116
104
  function textFlags(args, name) {
117
105
  return (args.flags.get(name) ?? []).filter((value) => typeof value === 'string');
118
106
  }
107
+ function sourceConnectorFlags(args) {
108
+ if (args.flags.has('no-source-connectors')) {
109
+ if (args.flags.has('source-connector'))
110
+ usage('--no-source-connectors and --source-connector are mutually exclusive');
111
+ return [];
112
+ }
113
+ return args.flags.has('source-connector') ? textFlags(args, 'source-connector') : undefined;
114
+ }
119
115
  function numberFlag(args, name) {
120
116
  const value = textFlag(args, name);
121
117
  if (value === undefined)
@@ -204,10 +200,9 @@ function output(value, json) {
204
200
  function showHelp() {
205
201
  process.stdout.write(`Usage:
206
202
  ai-hist [--no-bootstrap] [--db PATH] [--json] [--help]
207
- ai-hist enable-cloud [--base-url URL] [--token TOKEN] [--db PATH] [--interval SECONDS] [--once] [--json]
208
203
  ai-hist sessions list [--pretty] [--local | --remote | --all] [--source SOURCE]... [--limit N] [--before-ms MS] [--after JSON | --after-source SOURCE --after-session-id ID [--after-ms MS]] [--json]
209
- ai-hist sessions discover [--local | --remote | --all] [--source SOURCE] [--limit N] [--json]
210
- ai-hist sessions hydrate SOURCE SESSION_ID [--local | --remote | --all] [--no-related] [--db PATH] [--json]
204
+ ai-hist sessions discover [--local | --remote | --all] [--source-connector ID | --no-source-connectors] [--acquisition-timeout-ms N] [--source SOURCE] [--limit N] [--json]
205
+ ai-hist sessions hydrate SOURCE SESSION_ID [--local | --remote | --all] [--source-connector ID | --no-source-connectors] [--acquisition-timeout-ms N] [--no-related] [--db PATH] [--json]
211
206
  ai-hist sessions relationships SOURCE SESSION_ID [--db PATH] [--json]
212
207
  ai-hist sessions tree SOURCE SESSION_ID [--max-depth N] [--max-nodes N] [--db PATH] [--json]
213
208
  ai-hist sessions tools SOURCE SESSION_ID [--limit N] [--after JSON] [--db PATH] [--json]
@@ -218,11 +213,12 @@ function showHelp() {
218
213
  ai-hist events SESSION_ID [--source SOURCE] [--limit N] [--after JSON] [--json]
219
214
  ai-hist resume QUERY... [--local | --remote | --all] [--db PATH] [--fts] [--json]
220
215
  ai-hist pack QUERY... [--local | --remote | --all] [--source SOURCE] [--project PATH] [--tag TAG] [--limit N] [--tokens N] [--db PATH] [--fts] [--json]
221
- ai-hist login [--base-url URL] [--token TOKEN] [--label LABEL] [--json]
222
- ai-hist token [--base-url URL]
223
- ai-hist replay SESSION_ID [--base-url URL] [--limit N] [--max-content N] [--json] [--out PATH] [--help]
224
216
  ai-hist stats [--local | --remote | --all] [--json]
225
- ai-hist sync [--local | --remote | --all] [--db PATH] [--json]
217
+ ai-hist export --selection FILE [--out FILE] [--db PATH]
218
+ ai-hist delivery enable|drain|run --config FILE [--job ID] [--db PATH]
219
+ ai-hist delivery status|pause|resume|retry|cancel [--job ID] [--db PATH]
220
+ ai-hist plugin COMMAND --config FILE -- [ARGS...]
221
+ ai-hist sync [--local | --remote | --all] [--source-connector ID | --no-source-connectors] [--acquisition-timeout-ms N] [--db PATH] [--json]
226
222
 
227
223
  Every command that reads local history indexes it on first use; pass
228
224
  --no-bootstrap to answer from the store exactly as it stands.
@@ -234,10 +230,9 @@ function usage(message) {
234
230
  process.stderr.write(`ai-hist: ${message}\n\n`);
235
231
  process.stderr.write(`Usage:
236
232
  ai-hist [--no-bootstrap] [--db PATH] [--json] [--help]
237
- ai-hist enable-cloud [--base-url URL] [--token TOKEN] [--db PATH] [--interval SECONDS] [--once] [--json]
238
233
  ai-hist sessions list [--pretty] [--local | --remote | --all] [--source SOURCE]... [--limit N] [--before-ms MS] [--after JSON | --after-source SOURCE --after-session-id ID [--after-ms MS]] [--json]
239
- ai-hist sessions discover [--local | --remote | --all] [--source SOURCE] [--limit N] [--json]
240
- ai-hist sessions hydrate SOURCE SESSION_ID [--local | --remote | --all] [--no-related] [--db PATH] [--json]
234
+ ai-hist sessions discover [--local | --remote | --all] [--source-connector ID | --no-source-connectors] [--acquisition-timeout-ms N] [--source SOURCE] [--limit N] [--json]
235
+ ai-hist sessions hydrate SOURCE SESSION_ID [--local | --remote | --all] [--source-connector ID | --no-source-connectors] [--acquisition-timeout-ms N] [--no-related] [--db PATH] [--json]
241
236
  ai-hist sessions relationships SOURCE SESSION_ID [--db PATH] [--json]
242
237
  ai-hist sessions tree SOURCE SESSION_ID [--max-depth N] [--max-nodes N] [--db PATH] [--json]
243
238
  ai-hist sessions tools SOURCE SESSION_ID [--limit N] [--after JSON] [--db PATH] [--json]
@@ -248,11 +243,12 @@ function usage(message) {
248
243
  ai-hist events SESSION_ID [--source SOURCE] [--limit N] [--after JSON] [--json]
249
244
  ai-hist resume QUERY... [--local | --remote | --all] [--db PATH] [--fts] [--json]
250
245
  ai-hist pack QUERY... [--local | --remote | --all] [--source SOURCE] [--project PATH] [--tag TAG] [--limit N] [--tokens N] [--db PATH] [--fts] [--json]
251
- ai-hist login [--base-url URL] [--token TOKEN] [--label LABEL] [--json]
252
- ai-hist token [--base-url URL]
253
- ai-hist replay SESSION_ID [--base-url URL] [--limit N] [--max-content N] [--json] [--out PATH] [--help]
254
246
  ai-hist stats [--local | --remote | --all] [--json]
255
- ai-hist sync [--local | --remote | --all] [--db PATH] [--json]
247
+ ai-hist export --selection FILE [--out FILE] [--db PATH]
248
+ ai-hist delivery enable|drain|run --config FILE [--job ID] [--db PATH]
249
+ ai-hist delivery status|pause|resume|retry|cancel [--job ID] [--db PATH]
250
+ ai-hist plugin COMMAND --config FILE -- [ARGS...]
251
+ ai-hist sync [--local | --remote | --all] [--source-connector ID | --no-source-connectors] [--acquisition-timeout-ms N] [--db PATH] [--json]
256
252
 
257
253
  Every command that reads local history indexes it on first use; pass
258
254
  --no-bootstrap to answer from the store exactly as it stands.
@@ -549,17 +545,11 @@ function validateInterval(args) {
549
545
  // stops `search` and `ai-hist` disagreeing about whether a store exists.
550
546
  const COMMANDS = new Map([
551
547
  ['', { name: 'ai-hist', positionals: [0, 0], allowed: ['db', 'json', 'help'], readsLocalStore: true }],
552
- ['login', { name: 'login', positionals: [0, 0],
553
- allowed: ['base-url', 'json', 'label', 'token'],
554
- validate: (args) => {
555
- if (textFlag(args, 'token') && !textFlag(args, 'base-url'))
556
- usage('login requires --base-url with --token');
557
- } }],
558
- ['token', { name: 'token', positionals: [0, 0], allowed: ['base-url'] }],
559
- ['replay', { name: 'replay', positionals: [1, 1], requires: 'replay requires SESSION_ID',
560
- allowed: ['base-url', 'limit', 'max-content', 'json', 'out', 'help'] }],
561
- ['enable-cloud', { name: 'enable-cloud', positionals: [0, 0], validate: validateInterval,
562
- allowed: ['base-url', 'db', 'interval', 'once', 'json', 'token'] }],
548
+ ['export', { name: 'export', positionals: [0, 0], allowed: ['db', 'selection', 'out'] }],
549
+ ['plugin', { name: 'plugin', positionals: [1, null], allowed: ['config'], requires: 'plugin requires a command name' }],
550
+ ...['enable', 'status', 'drain', 'run', 'pause', 'resume', 'retry', 'cancel'].map((action) => [`delivery ${action}`, {
551
+ name: `delivery ${action}`, positionals: [0, 0], allowed: ['db', 'config', 'job', 'poll-ms', 'timeout-ms'],
552
+ }]),
563
553
  ['sessions list', { name: 'sessions list', positionals: [0, 0], readsLocalStore: true,
564
554
  validate: (args) => {
565
555
  if (args.flags.has('json') && args.flags.has('pretty'))
@@ -570,10 +560,12 @@ const COMMANDS = new Map([
570
560
  'json', 'limit', 'local', 'pretty', 'remote', 'source',
571
561
  ] }],
572
562
  ['sessions discover', { name: 'sessions discover', positionals: [0, 0],
573
- allowed: ['all', 'db', 'json', 'limit', 'local', 'remote', 'source'] }],
574
- ['sessions hydrate', { name: 'sessions hydrate', positionals: [2, 2], readsLocalStore: true,
563
+ validate: (args) => { sourceConnectorFlags(args); },
564
+ allowed: ['all', 'db', 'json', 'limit', 'local', 'remote', 'source', 'config', 'source-connector', 'no-source-connectors', 'acquisition-timeout-ms'] }],
565
+ ['sessions hydrate', { name: 'sessions hydrate', positionals: [2, 2],
575
566
  requires: 'sessions hydrate requires SOURCE and SESSION_ID',
576
- allowed: ['all', 'db', 'json', 'local', 'no-related', 'remote'] }],
567
+ validate: (args) => { sourceConnectorFlags(args); },
568
+ allowed: ['all', 'db', 'json', 'local', 'no-bootstrap', 'no-related', 'remote', 'config', 'source-connector', 'no-source-connectors', 'acquisition-timeout-ms'] }],
577
569
  ['sessions relationships', { name: 'sessions relationships', positionals: [2, 2], readsLocalStore: true,
578
570
  rejectsScope: true, requires: 'sessions relationships requires SOURCE and SESSION_ID',
579
571
  allowed: ['all', 'db', 'json', 'local', 'remote'] }],
@@ -609,35 +601,36 @@ const COMMANDS = new Map([
609
601
  allowed: ['all', 'db', 'json', 'local', 'remote', 'tag'] }],
610
602
  // sync and `sessions discover` build the store rather than read it, so they
611
603
  // do not bootstrap first; running them is itself the remedy for an empty one.
612
- ['sync', { name: 'sync', positionals: [0, 0], allowed: ['all', 'db', 'json', 'local', 'remote'] }],
604
+ ['sync', { name: 'sync', positionals: [0, 0], validate: (args) => { sourceConnectorFlags(args); },
605
+ allowed: ['all', 'db', 'json', 'local', 'remote', 'config', 'source-connector', 'no-source-connectors', 'acquisition-timeout-ms'] }],
613
606
  ]);
614
607
  /** Command words consumed before the positional arguments start. */
615
608
  function commandWords(command) {
616
609
  if (command === undefined)
617
610
  return 0;
618
- return command === 'sessions' ? 2 : 1;
611
+ return command === 'sessions' || command === 'delivery' ? 2 : 1;
619
612
  }
620
613
  function commandSpec(command, subcommand) {
621
614
  if (command === undefined)
622
615
  return COMMANDS.get('');
623
- if (command === 'sessions')
624
- return subcommand ? COMMANDS.get(`sessions ${subcommand}`) : undefined;
616
+ if (command === 'sessions' || command === 'delivery')
617
+ return subcommand ? COMMANDS.get(`${command} ${subcommand}`) : undefined;
625
618
  return COMMANDS.get(command);
626
619
  }
627
620
  function unknownCommandMessage(command, subcommand) {
628
621
  if (command === undefined)
629
622
  return 'invalid usage';
630
- if (command === 'sessions') {
623
+ if (command === 'sessions' || command === 'delivery') {
631
624
  if (subcommand === undefined)
632
- return 'sessions requires a subcommand';
633
- return `unknown sessions subcommand '${subcommand}'`;
625
+ return `${command} requires a subcommand`;
626
+ return `unknown ${command} subcommand '${subcommand}'`;
634
627
  }
635
628
  return `unknown command '${command}'`;
636
629
  }
637
630
  // A store that was never built and a store holding no match are different
638
631
  // answers, and `No results.` is only true of the second. Wording and exit code
639
632
  // separate them; the query is not run against a store that cannot hold one.
640
- /** `--all` may skip remote and answer from local even when the local store is empty. */
633
+ /** Cached remote evidence can answer an all-scope query even when local history is empty. */
641
634
  function skipUnusableStoreGate(spec, scope) {
642
635
  if (scope !== 'all')
643
636
  return false;
@@ -668,7 +661,12 @@ async function main() {
668
661
  await maybePrintUpdateNotice(version, rawArgs);
669
662
  return;
670
663
  }
671
- const args = parse(rawArgs.map((arg) => arg === '-h' ? '--help' : arg));
664
+ const boundary = rawArgs.indexOf('--');
665
+ const beforeBoundary = boundary < 0 ? rawArgs : rawArgs.slice(0, boundary);
666
+ const separator = boundary >= 0 && parse(beforeBoundary).positional[0] === 'plugin' ? boundary : -1;
667
+ const pluginArgs = separator < 0 ? [] : rawArgs.slice(separator + 1);
668
+ const coreArgs = separator < 0 ? rawArgs : rawArgs.slice(0, separator);
669
+ const args = parse(coreArgs.map((arg) => arg === '-h' ? '--help' : arg));
672
670
  const [command, subcommand, ...rest] = args.positional;
673
671
  const json = args.flags.has('json');
674
672
  // Handle help before command resolution to prevent unknown command errors
@@ -678,7 +676,7 @@ async function main() {
678
676
  if (command === 'help' && subcommand === undefined && rest.length === 0) {
679
677
  showHelp();
680
678
  }
681
- if (command === 'sessions' && subcommand === undefined && args.flags.has('help')) {
679
+ if ((command === 'sessions' || command === 'delivery') && subcommand === undefined && args.flags.has('help')) {
682
680
  showHelp();
683
681
  }
684
682
  const spec = commandSpec(command, subcommand);
@@ -704,6 +702,30 @@ async function main() {
704
702
  const sessionId = command === 'sessions' ? tail[1] : undefined;
705
703
  const recentFallback = command === 'recent' && tail.length > 0 ? Number(tail[0]) : undefined;
706
704
  const scope = scopeFlag(args);
705
+ if (command === 'delivery') {
706
+ await runDeliveryCommand(subcommand, { dbPath: textFlag(args, 'db'), configPath: textFlag(args, 'config'),
707
+ jobId: textFlag(args, 'job'), pollIntervalMs: numberFlag(args, 'poll-ms'), requestTimeoutMs: numberFlag(args, 'timeout-ms') });
708
+ return;
709
+ }
710
+ if (command === 'export') {
711
+ const selectionPath = textFlag(args, 'selection');
712
+ if (!selectionPath)
713
+ usage('export requires --selection FILE');
714
+ await runHistoryExportCommand({ dbPath: textFlag(args, 'db'), selectionPath, outputPath: textFlag(args, 'out') });
715
+ return;
716
+ }
717
+ if (command === 'plugin') {
718
+ const configPath = textFlag(args, 'config');
719
+ if (!configPath)
720
+ usage('plugin requires --config FILE');
721
+ const { registry } = await loadHistoryApplicationConfig(configPath);
722
+ const operation = registry.command(tail[0]);
723
+ if (!operation)
724
+ usage('configured plugin command not found');
725
+ output(await operation.run([...tail.slice(1), ...pluginArgs]), true);
726
+ return;
727
+ }
728
+ const acquisitionPlugins = ['sync', 'sessions'].includes(command ?? '') && textFlag(args, 'config') ? (await loadHistoryApplicationConfig(textFlag(args, 'config'))).registry : undefined;
707
729
  let readiness = null;
708
730
  if (spec.readsLocalStore) {
709
731
  readiness = await ensureLocalStore({
@@ -734,53 +756,6 @@ async function main() {
734
756
  }
735
757
  return;
736
758
  }
737
- if (command === 'login') {
738
- const relayAccessToken = await relayAccessTokenForCloudCommand(command, rawArgs, args, false);
739
- const auth = await login({
740
- baseUrl: textFlag(args, 'base-url'),
741
- relayAccessToken,
742
- label: textFlag(args, 'label'),
743
- });
744
- if (json)
745
- output({ ok: true, baseUrl: auth.baseUrl }, true);
746
- else
747
- process.stdout.write(`Logged in to ${auth.baseUrl} (session stored).\n`);
748
- return;
749
- }
750
- if (command === 'token') {
751
- const token = await accessToken({ baseUrl: textFlag(args, 'base-url') });
752
- if (process.stdout.isTTY)
753
- process.stderr.write('Warning: this access token is a secret and will remain in terminal scrollback.\n');
754
- process.stdout.write(`${token}\n`);
755
- return;
756
- }
757
- if (command === 'replay') {
758
- const result = await replay(subcommand, {
759
- baseUrl: textFlag(args, 'base-url'), limit: nonNegativeIntFlag(args, 'limit'),
760
- maxContent: nonNegativeIntFlag(args, 'max-content'), json, out: textFlag(args, 'out'),
761
- });
762
- if (result.transcript !== null)
763
- process.stdout.write(result.transcript);
764
- return;
765
- }
766
- if (command === 'enable-cloud') {
767
- const relayAccessToken = await relayAccessTokenForCloudCommand(command, rawArgs, args, true);
768
- const handle = await enableCloud({
769
- baseUrl: textFlag(args, 'base-url'), dbPath: textFlag(args, 'db'),
770
- relayAccessToken,
771
- intervalMs: intervalSeconds * 1000,
772
- watch: !args.flags.has('once'),
773
- onPush: (result) => output(result, json),
774
- });
775
- const { stop, ...result } = handle;
776
- output(result, json);
777
- if (!args.flags.has('once')) {
778
- const shutdown = () => { void stop(); };
779
- process.once('SIGINT', shutdown);
780
- process.once('SIGTERM', shutdown);
781
- }
782
- return;
783
- }
784
759
  if (command === 'sessions' && subcommand === 'list') {
785
760
  const sources = textFlags(args, 'source');
786
761
  const page = await listSessionCatalogPage({
@@ -803,6 +778,7 @@ async function main() {
803
778
  if (command === 'sessions' && subcommand === 'discover') {
804
779
  const sources = textFlags(args, 'source');
805
780
  outputDiscovery(await discoverSessions({
781
+ sourceConnectors: sourceConnectorFlags(args), acquisitionTimeoutMs: numberFlag(args, 'acquisition-timeout-ms'), plugins: acquisitionPlugins,
806
782
  dbPath: textFlag(args, 'db'), scope: scopeFlag(args), sources: sources.length ? sources : undefined,
807
783
  limit: numberFlag(args, 'limit'),
808
784
  }), json);
@@ -810,6 +786,7 @@ async function main() {
810
786
  }
811
787
  if (command === 'sessions' && subcommand === 'hydrate') {
812
788
  outputHydration(await hydrateSession({
789
+ sourceConnectors: sourceConnectorFlags(args), acquisitionTimeoutMs: numberFlag(args, 'acquisition-timeout-ms'), plugins: acquisitionPlugins,
813
790
  source: sessionSource,
814
791
  sessionId: sessionId,
815
792
  scope: scopeFlag(args),
@@ -878,7 +855,7 @@ async function main() {
878
855
  return;
879
856
  }
880
857
  if (command === 'sync') {
881
- output(await sync({ dbPath: textFlag(args, 'db'), scope: scopeFlag(args) }), json);
858
+ output(await sync({ dbPath: textFlag(args, 'db'), scope: scopeFlag(args), sourceConnectors: sourceConnectorFlags(args), acquisitionTimeoutMs: numberFlag(args, 'acquisition-timeout-ms'), plugins: acquisitionPlugins }), json);
882
859
  return;
883
860
  }
884
861
  usage();