@trydreamstate/cli 1.3.2 → 1.3.3

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 (75) hide show
  1. package/README.md +29 -16
  2. package/agent-plugin/.claude-plugin/plugin.json +1 -1
  3. package/agent-plugin/.codex-plugin/plugin.json +1 -1
  4. package/agent-plugin/hooks/approve-readonly.sh +15 -0
  5. package/agent-plugin/hooks/hooks.json +17 -0
  6. package/agent-plugin/skills/crm/SKILL.md +2 -2
  7. package/agent-plugin/skills/diagnostics/SKILL.md +1 -1
  8. package/agent-plugin/skills/dreamstate/SKILL.md +13 -5
  9. package/agent-plugin/skills/first-win/SKILL.md +2 -2
  10. package/agent-plugin/skills/inbox-replies/SKILL.md +1 -1
  11. package/agent-plugin/skills/knowledge/SKILL.md +1 -1
  12. package/agent-plugin/skills/linkedin-posts/SKILL.md +1 -1
  13. package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +2 -2
  14. package/agent-plugin/skills/list-building/SKILL.md +4 -4
  15. package/agent-plugin/skills/outreach/SKILL.md +1 -1
  16. package/agent-plugin/skills/outreach/steps-and-timing.md +1 -1
  17. package/agent-plugin/skills/plays/SKILL.md +2 -2
  18. package/agent-plugin/skills/reddit-posts/SKILL.md +1 -1
  19. package/agent-plugin/skills/skill-authoring/SKILL.md +1 -1
  20. package/agent-plugin/skills/social/SKILL.md +2 -2
  21. package/agent-plugin/skills/visibility/SKILL.md +2 -2
  22. package/agent-plugin/skills/workflows/SKILL.md +3 -3
  23. package/agent-plugin/skills/worksheet-tidy/SKILL.md +1 -1
  24. package/agent-plugin/skills/writing/SKILL.md +1 -1
  25. package/agent-plugin/skills/x-posts/SKILL.md +1 -1
  26. package/dist/agentGuidance.generated.json +35 -0
  27. package/dist/client.d.ts +31 -2
  28. package/dist/client.d.ts.map +1 -1
  29. package/dist/client.js +115 -33
  30. package/dist/commandTable.d.ts +26 -0
  31. package/dist/commandTable.d.ts.map +1 -0
  32. package/dist/commandTable.js +54 -0
  33. package/dist/commands/sequences.d.ts +1 -1
  34. package/dist/commands/sequences.d.ts.map +1 -1
  35. package/dist/commands/sequences.js +37 -36
  36. package/dist/commands/shared.d.ts +4 -26
  37. package/dist/commands/shared.d.ts.map +1 -1
  38. package/dist/commands/shared.js +129 -127
  39. package/dist/commands/workflows.d.ts +1 -1
  40. package/dist/commands/workflows.d.ts.map +1 -1
  41. package/dist/commands/workflows.js +35 -41
  42. package/dist/errors.d.ts +78 -0
  43. package/dist/errors.d.ts.map +1 -0
  44. package/dist/errors.js +117 -0
  45. package/dist/gatedRun.d.ts +81 -0
  46. package/dist/gatedRun.d.ts.map +1 -0
  47. package/dist/gatedRun.js +370 -0
  48. package/dist/idempotency.d.ts +38 -0
  49. package/dist/idempotency.d.ts.map +1 -0
  50. package/dist/idempotency.js +106 -0
  51. package/dist/index.d.ts +21 -35
  52. package/dist/index.d.ts.map +1 -1
  53. package/dist/index.js +372 -366
  54. package/dist/runWait.d.ts +2 -1
  55. package/dist/runWait.d.ts.map +1 -1
  56. package/dist/runWait.js +3 -1
  57. package/dist/schemaCoercion.d.ts +20 -0
  58. package/dist/schemaCoercion.d.ts.map +1 -0
  59. package/dist/schemaCoercion.js +123 -0
  60. package/dist/skillInstaller.d.ts +40 -0
  61. package/dist/skillInstaller.d.ts.map +1 -1
  62. package/dist/skillInstaller.js +108 -9
  63. package/dist/tableFilePreview.d.ts +14 -0
  64. package/dist/tableFilePreview.d.ts.map +1 -1
  65. package/dist/tableFilePreview.js +19 -8
  66. package/dist/tableFileReader.d.ts +26 -0
  67. package/dist/tableFileReader.d.ts.map +1 -0
  68. package/dist/tableFileReader.js +108 -0
  69. package/dist/tablesImport.d.ts +57 -0
  70. package/dist/tablesImport.d.ts.map +1 -0
  71. package/dist/tablesImport.js +263 -0
  72. package/dist/turnCache.d.ts +14 -1
  73. package/dist/turnCache.d.ts.map +1 -1
  74. package/dist/turnCache.js +25 -8
  75. package/package.json +3 -2
package/dist/errors.js ADDED
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CLI_LOCAL_ERROR_REPAIRS = exports.CLI_LOCAL_ERROR_CODES = exports.CLI_EXIT = exports.CliUsageError = void 0;
4
+ exports.isApprovalGateCode = isApprovalGateCode;
5
+ exports.isLocalRefusalCode = isLocalRefusalCode;
6
+ class CliUsageError extends Error {
7
+ constructor(message) {
8
+ super(message);
9
+ this.name = 'CliUsageError';
10
+ }
11
+ }
12
+ exports.CliUsageError = CliUsageError;
13
+ /**
14
+ * Process exit codes. The numbers are a published contract; change them only
15
+ * with a major version. `dreamstate --help` renders their stable meanings.
16
+ *
17
+ * Contract C5 (cross-surface): 0 ok, 1 generic, 2 usage, 3 auth, 4 not found,
18
+ * 5 conflict/validation, 6 rate limited, 7 approval/consent/spend gate required,
19
+ * 8 cli_update_required, 9 other refusal. Where this CLI had already published a
20
+ * different meaning it kept the published one and the mapping is documented:
21
+ *
22
+ * - 5 stays "server failure" (HTTP 5xx and unreadable responses). A 4xx
23
+ * conflict or validation refusal exits 9, as published and pinned by
24
+ * exitCodes.test.ts: a script must not read a caller mistake as a broken server.
25
+ * - 8 is cli_update_required. It was "request timeout" before; timeouts are 11
26
+ * now, so a script can tell "update the CLI" from "try again later".
27
+ */
28
+ exports.CLI_EXIT = {
29
+ SUCCESS: 0,
30
+ UNCLASSIFIED: 1,
31
+ USAGE: 2,
32
+ AUTH: 3,
33
+ NOT_FOUND: 4,
34
+ SERVER: 5,
35
+ RATE_LIMITED: 6,
36
+ APPROVAL: 7,
37
+ UPDATE_REQUIRED: 8,
38
+ REFUSED: 9,
39
+ TRANSPORT: 10,
40
+ TIMEOUT: 11,
41
+ };
42
+ /**
43
+ * Codes the CLI process raises itself, with no server answer behind them.
44
+ * The server never emits these. They are declared here, once, so the README
45
+ * table and the tests that keep every code in CLI source accounted for
46
+ * (errorCodes.test.ts) read one list. A code that is not in this table must be
47
+ * a member of the shared API_ERROR_CODES.
48
+ */
49
+ exports.CLI_LOCAL_ERROR_CODES = {
50
+ network_error: 'The request never reached the server (DNS, connection refused, TLS). Nothing executed.',
51
+ request_timeout: 'The server did not answer in time. The effect may have happened. Read the target back first. If the effect is there, report it done. If not, repeat the identical call once with the first attempt\'s idempotency key, never a new one. If the read-back is inconclusive, or the repeat is refused pending acknowledgement, ask the person.',
52
+ invalid_response: 'The server answered with something that is not a Dreamstate envelope.',
53
+ invalid_tool_turn_token: 'tools get returned no tool_turn_token to run with.',
54
+ invalid_web_url: 'The API returned a web_url that is not a safe http(s) URL.',
55
+ web_url_unavailable: '--open was given but the response carries no web_url.',
56
+ plugin_install_failed: 'dreamstate install could not install the plugin for any agent.',
57
+ skills_authentication_required: 'skills install needs a stored login.',
58
+ };
59
+ /** Recovery metadata for failures raised by this CLI, rather than the API. */
60
+ exports.CLI_LOCAL_ERROR_REPAIRS = {
61
+ network_error: { kind: 'wait', hint: 'Retry the unchanged request when network connectivity is available.' },
62
+ request_timeout: { kind: 'ask_user', hint: 'Read the target back first. If absent, repeat once with the same idempotency key; otherwise ask the person.' },
63
+ invalid_response: { kind: 'server_defect', hint: 'The API response was not a Dreamstate envelope; report the trace id if one was returned.' },
64
+ invalid_tool_turn_token: { kind: 'server_defect', hint: 'The API did not provide a runnable tool turn; retry after reporting the response trace id.' },
65
+ invalid_web_url: { kind: 'server_defect', hint: 'The API returned an invalid web URL; report the response trace id.' },
66
+ web_url_unavailable: { kind: 'not_possible', hint: 'This result has no web URL to open.' },
67
+ plugin_install_failed: { kind: 'ask_user', hint: 'Install a supported agent, then retry plugin installation.' },
68
+ skills_authentication_required: { kind: 'ask_user', hint: 'Sign in with dreamstate login, then retry.' },
69
+ };
70
+ /**
71
+ * Refusals that mean a person (or the caller's own spend decision) must supply
72
+ * something before the run can proceed: an approval, a consent, a quote or a
73
+ * spend ceiling. They exit 7, whatever HTTP status carried them, so a script
74
+ * can stop and ask instead of retrying.
75
+ */
76
+ const GATE_CODES = new Set([
77
+ 'approval_required',
78
+ 'standing_approval_required',
79
+ 'destructive_approval_required',
80
+ 'owner_approval_required',
81
+ 'knowledge_approval_required',
82
+ 'content_approval_required',
83
+ 'effect_unknown_approval_required',
84
+ 'approval_cap_exceeded',
85
+ 'approval_stale',
86
+ 'agent_tool_policy_ask_before_changes',
87
+ 'publish_confirmation_required',
88
+ 'launch_authorization_required',
89
+ 'publish_permission_required',
90
+ 'delegation_grant_required',
91
+ 'creator_consent_required',
92
+ 'csv_replace_confirmation_required',
93
+ 'quote_required',
94
+ 'quote_mismatch',
95
+ 'quote_stale',
96
+ 'quote_already_used',
97
+ 'spend_cap',
98
+ 'spend_cap_required',
99
+ 'spend_cap_too_low',
100
+ 'cost_cap_exceeded',
101
+ 'agent_paid_capability_requires_max_credits',
102
+ 'max_rows_required',
103
+ 'mode_required',
104
+ 'sequence_send_cap_required',
105
+ ]);
106
+ const GATE_CODE_PATTERN = /(?:approval|consent|confirmation|authorization)_required$|requires_approval$/;
107
+ function isApprovalGateCode(code) {
108
+ return Boolean(code) && (GATE_CODES.has(code) || GATE_CODE_PATTERN.test(code));
109
+ }
110
+ /**
111
+ * Refusals the CLI decides locally, before running anything, that are not usage
112
+ * errors: the capability is not published for the CLI surface.
113
+ */
114
+ const LOCAL_REFUSAL_CODES = new Set(['unsupported_capability']);
115
+ function isLocalRefusalCode(code) {
116
+ return Boolean(code) && LOCAL_REFUSAL_CODES.has(code);
117
+ }
@@ -0,0 +1,81 @@
1
+ import { CliError, type DreamstateClient } from './client';
2
+ import { type IdempotencyIdentity } from './idempotency';
3
+ export declare function code(error: unknown): string | undefined;
4
+ export declare function toolTurnToken(value: unknown): string;
5
+ export declare function omitsToolTurnForInlineTableRead(capabilityId: string): boolean;
6
+ /** The capability record inside a tools get response, or null when the shape is unknown. */
7
+ export declare function capabilityRecord(fetched: unknown): Record<string, unknown> | null;
8
+ /**
9
+ * Header value, or null when the request must not carry Idempotency-Key.
10
+ * Any key forces a read onto the durable run path (isInlineReadEligible).
11
+ * Send one only for an explicit key, or when the tools get contract says
12
+ * mutates or idempotency_required. Missing flags omit the key: a read whose
13
+ * contract does not set mutates must not be treated as a write.
14
+ *
15
+ * A write's key is scoped to an attempt (idempotency.ts). A pending interrupted
16
+ * attempt reuses its key; a terminal attempt gets a fresh key on a later command.
17
+ */
18
+ export declare function capabilityIdempotencyKey(fetched: unknown, explicit: string | undefined, identity: IdempotencyIdentity): string | null;
19
+ export declare function parameterIdempotencyKey(body: Record<string, unknown>): string | undefined;
20
+ /**
21
+ * Whether the capability is published for the CLI surface. The tools get
22
+ * contract lists the CLI commands the catalog advertises (cli_commands); an
23
+ * empty list means the capability is not on this surface. This reads the
24
+ * field the contract carries today; when the shared surface-visibility
25
+ * function (contract C7) reaches the contract this is the one place to
26
+ * call it. A contract that says nothing about the CLI is allowed, because a
27
+ * missing field is not a refusal.
28
+ */
29
+ export declare function cliSurfaceRefusal(capabilityId: string, fetched: unknown): CliError | null;
30
+ /**
31
+ * What a quote prices: the params plus the ceiling. Keys that say how a call is
32
+ * made, not what it does, are left out, like the server's own plan digest.
33
+ */
34
+ export declare function planFingerprint(body: Record<string, unknown>): string;
35
+ export interface GatedRunContext {
36
+ client: DreamstateClient;
37
+ baseUrl: string;
38
+ /** apiKey plus workspace: a turn belongs to one workspace, so it is part of the cache identity. */
39
+ turnKey: string;
40
+ turnsPath: string;
41
+ /** Pending live-attempt keys, stored next to CLI credentials. */
42
+ idempotencyPath?: string;
43
+ workspace?: string;
44
+ stderr: NodeJS.WritableStream;
45
+ sleep: (milliseconds: number) => Promise<void>;
46
+ }
47
+ export interface GatedRunInput {
48
+ capabilityId: string;
49
+ /** Door body: params plus mode, approved, max_credits, quote_hash, approval_id ... */
50
+ body: Record<string, unknown>;
51
+ /** --turn-token: a token the caller already holds from tools get. */
52
+ turnToken?: string;
53
+ /** --idempotency-key: used as given. Without it a write gets a deterministic key. */
54
+ idempotencyKey?: string;
55
+ waitTimeout: number;
56
+ noWait?: boolean;
57
+ /**
58
+ * The params came from shell flags (pack verbs), so each string value is
59
+ * converted to the type the capability's live input schema declares.
60
+ */
61
+ coerceParams?: boolean;
62
+ /**
63
+ * Last edit of the params once the live capability contract is known. Used by
64
+ * pack verbs to bridge a capability that still declares a gating input inside
65
+ * its params (contract C6 keeps those at the door): the verb copies the door
66
+ * value into params only while the contract's input schema still lists it.
67
+ */
68
+ adjustParams?: (params: Record<string, unknown>, capability: Record<string, unknown> | null) => Record<string, unknown>;
69
+ }
70
+ export interface GatedRunOutcome {
71
+ value: unknown;
72
+ timedOut: boolean;
73
+ /** The Idempotency-Key the run was sent with, or null when none applied. */
74
+ idempotencyKey: string | null;
75
+ /** The tool_turn_token the run used, so a caller making many runs of one capability can reuse it. */
76
+ turnToken: string | null;
77
+ }
78
+ export declare function runGated(context: GatedRunContext, input: GatedRunInput): Promise<GatedRunOutcome>;
79
+ /** A read the CLI needs before it can form a write, run through the same gate as any other run. */
80
+ export declare function readCapability(context: GatedRunContext, capabilityId: string, params: Record<string, unknown>, waitTimeout: number): Promise<unknown>;
81
+ //# sourceMappingURL=gatedRun.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gatedRun.d.ts","sourceRoot":"","sources":["../src/gatedRun.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,370 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.code = code;
4
+ exports.toolTurnToken = toolTurnToken;
5
+ exports.omitsToolTurnForInlineTableRead = omitsToolTurnForInlineTableRead;
6
+ exports.capabilityRecord = capabilityRecord;
7
+ exports.capabilityIdempotencyKey = capabilityIdempotencyKey;
8
+ exports.parameterIdempotencyKey = parameterIdempotencyKey;
9
+ exports.cliSurfaceRefusal = cliSurfaceRefusal;
10
+ exports.planFingerprint = planFingerprint;
11
+ exports.runGated = runGated;
12
+ exports.readCapability = readCapability;
13
+ const node_crypto_1 = require("node:crypto");
14
+ const client_1 = require("./client");
15
+ const errors_1 = require("./errors");
16
+ const idempotency_1 = require("./idempotency");
17
+ const schemaCoercion_1 = require("./schemaCoercion");
18
+ const runWait_1 = require("./runWait");
19
+ const turnCache_1 = require("./turnCache");
20
+ /**
21
+ * The one path every capability run takes: `tools run` and every pack verb
22
+ * (`sequences ...`, `workflows ...`) call runGated. Gating lives here and only
23
+ * here (contract C6): mode, max_credits, quote_hash, approved, approval_id and
24
+ * the idempotency key are door inputs, handled once, so a verb can never be a
25
+ * quieter way to run what `tools run` would stop.
26
+ */
27
+ function record(value) {
28
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
29
+ }
30
+ function code(error) {
31
+ return error && typeof error === 'object' && typeof error.code === 'string'
32
+ ? error.code
33
+ : undefined;
34
+ }
35
+ function toolTurnToken(value) {
36
+ const root = value && typeof value === 'object' ? value : {};
37
+ const data = root.data && typeof root.data === 'object' ? root.data : {};
38
+ if (typeof data.tool_turn_token === 'string' && data.tool_turn_token.trim())
39
+ return data.tool_turn_token;
40
+ throw new client_1.CliError('tools get did not return a tool_turn_token.', 'invalid_tool_turn_token', 0);
41
+ }
42
+ /**
43
+ * Free sync reads. The server runs these on the inline read path when
44
+ * the body has no tool_turn_token and the request has no Idempotency-Key
45
+ * (isInlineReadEligible). A tools get before the run is a second HTTP the
46
+ * read does not need. Measured 2026-10-04: tables.workbooks.get full live CLI
47
+ * 0.726s against whoami 0.455s, about 0.27s of tools get per harness read-back.
48
+ * knowledge.brain.context.get is the same class (catalog: credit_class free,
49
+ * async_policy.mode sync, mutates not set). Live 2026-10-05 a standalone
50
+ * tools get of that id was 0.400s including node startup, next to whoami
51
+ * 0.422s, so the same-process skip is one HTTP, not a second process.
52
+ * The set name is historical. Writes are not in this set: they stay fetch-first.
53
+ */
54
+ const INLINE_READ_CAPABILITIES = new Set([
55
+ 'tables.workbooks.get',
56
+ 'tables.workbooks.overview',
57
+ 'tables.workbooks.list',
58
+ 'tables.columns.list',
59
+ 'tables.columns.stats',
60
+ 'tables.rows.query',
61
+ 'tables.rows.get',
62
+ 'tables.rows.count',
63
+ 'knowledge.brain.context.get',
64
+ // Both are stored, free, synchronous inbox reads. Keeping either behind a
65
+ // tools_get turns one list or thread open into two HTTP requests, while the
66
+ // canonical inline-read gate already validates their read-only contract.
67
+ 'unibox.inbox.unified_list',
68
+ 'unibox.inbox.conversation_get',
69
+ ]);
70
+ function omitsToolTurnForInlineTableRead(capabilityId) {
71
+ return INLINE_READ_CAPABILITIES.has(capabilityId);
72
+ }
73
+ /** The capability record inside a tools get response, or null when the shape is unknown. */
74
+ function capabilityRecord(fetched) {
75
+ const root = record(fetched);
76
+ if (!root)
77
+ return null;
78
+ const data = record(root.data);
79
+ return record(data?.capability) ?? record(root.capability);
80
+ }
81
+ function readIdempotencyContract(fetched) {
82
+ if (!fetched || typeof fetched !== 'object' || Array.isArray(fetched))
83
+ return null;
84
+ const root = fetched;
85
+ const data = record(root.data);
86
+ const candidates = [data?.capability, root.capability, data, root];
87
+ for (const candidate of candidates) {
88
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
89
+ continue;
90
+ if ('mutates' in candidate || 'idempotency_required' in candidate) {
91
+ return candidate;
92
+ }
93
+ }
94
+ return null;
95
+ }
96
+ /**
97
+ * Header value, or null when the request must not carry Idempotency-Key.
98
+ * Any key forces a read onto the durable run path (isInlineReadEligible).
99
+ * Send one only for an explicit key, or when the tools get contract says
100
+ * mutates or idempotency_required. Missing flags omit the key: a read whose
101
+ * contract does not set mutates must not be treated as a write.
102
+ *
103
+ * A write's key is scoped to an attempt (idempotency.ts). A pending interrupted
104
+ * attempt reuses its key; a terminal attempt gets a fresh key on a later command.
105
+ */
106
+ function capabilityIdempotencyKey(fetched, explicit, identity) {
107
+ if (typeof explicit === 'string' && explicit.length > 0)
108
+ return explicit;
109
+ const contract = readIdempotencyContract(fetched);
110
+ if (contract?.mutates === true || contract?.idempotency_required === true)
111
+ return (0, idempotency_1.deterministicIdempotencyKey)(identity);
112
+ return null;
113
+ }
114
+ function parameterIdempotencyKey(body) {
115
+ const params = body.params;
116
+ if (!params || typeof params !== 'object' || Array.isArray(params))
117
+ return undefined;
118
+ const key = params.idempotency_key;
119
+ return typeof key === 'string' && key.length > 0 ? key : undefined;
120
+ }
121
+ /**
122
+ * Whether the capability is published for the CLI surface. The tools get
123
+ * contract lists the CLI commands the catalog advertises (cli_commands); an
124
+ * empty list means the capability is not on this surface. This reads the
125
+ * field the contract carries today; when the shared surface-visibility
126
+ * function (contract C7) reaches the contract this is the one place to
127
+ * call it. A contract that says nothing about the CLI is allowed, because a
128
+ * missing field is not a refusal.
129
+ */
130
+ function cliSurfaceRefusal(capabilityId, fetched) {
131
+ const capability = capabilityRecord(fetched);
132
+ if (!capability)
133
+ return null;
134
+ const commands = capability.cli_commands;
135
+ const surfaces = record(capability.surfaces);
136
+ const absent = Array.isArray(commands) ? commands.length === 0 : surfaces?.cli === false;
137
+ if (!absent)
138
+ return null;
139
+ return new client_1.CliError(`${capabilityId} is not available on the CLI surface. Use the web app, MCP or the Architect for it.`, 'unsupported_capability', 0, { details: { capability_id: capabilityId, surface: 'cli' }, retryable: false });
140
+ }
141
+ const QUOTE_HASH_PATTERN = /^sha256:[a-f0-9]{64}$/;
142
+ /**
143
+ * What a quote prices: the params plus the ceiling. Keys that say how a call is
144
+ * made, not what it does, are left out, like the server's own plan digest.
145
+ */
146
+ function planFingerprint(body) {
147
+ const params = body.params && typeof body.params === 'object' && !Array.isArray(body.params)
148
+ ? { ...body.params }
149
+ : {};
150
+ for (const key of ['quote_hash', 'plan_digest', 'estimate_digest', 'approved', 'max_credits', 'mode', 'dry_run', 'idempotency_key', 'tool_turn_token'])
151
+ delete params[key];
152
+ return (0, node_crypto_1.createHash)('sha256').update((0, idempotency_1.stableJson)({ params, max_credits: body.max_credits ?? null })).digest('hex');
153
+ }
154
+ /** The quote a dry run returned, wherever the envelope nests its outputs. */
155
+ function quoteFromResult(value, depth = 0) {
156
+ if (!value || typeof value !== 'object' || depth > 6)
157
+ return null;
158
+ if (Array.isArray(value)) {
159
+ for (const entry of value) {
160
+ const found = quoteFromResult(entry, depth + 1);
161
+ if (found)
162
+ return found;
163
+ }
164
+ return null;
165
+ }
166
+ const entry = value;
167
+ if (typeof entry.quote_hash === 'string' && QUOTE_HASH_PATTERN.test(entry.quote_hash)) {
168
+ const estimate = entry.estimate && typeof entry.estimate === 'object' ? entry.estimate : null;
169
+ return {
170
+ quote_hash: entry.quote_hash,
171
+ maximum_credits: typeof estimate?.maximum_credits === 'number' ? estimate.maximum_credits : null,
172
+ };
173
+ }
174
+ for (const key of ['data', 'run', 'outputs', 'steps']) {
175
+ const found = quoteFromResult(entry[key], depth + 1);
176
+ if (found)
177
+ return found;
178
+ }
179
+ return null;
180
+ }
181
+ function quoteSummary(capabilityId, body, quote) {
182
+ const params = body.params && typeof body.params === 'object' ? body.params : {};
183
+ const rows = typeof params.max_rows === 'number' ? `${params.max_rows} rows` : 'rows not stated';
184
+ const credits = quote.maximum_credits === null ? 'credits not stated' : `up to ${quote.maximum_credits} credits`;
185
+ return `Dry run of ${capabilityId} priced: ${rows}, ${credits}. Running live on that quote.`;
186
+ }
187
+ /** Adds the idempotency key to the printed meta, so a user can see which key to re-run with. */
188
+ function withIdempotencyMeta(value, key) {
189
+ const envelope = record(value);
190
+ if (!envelope || !key)
191
+ return value;
192
+ return { ...envelope, meta: { ...(record(envelope.meta) ?? {}), idempotency_key: key } };
193
+ }
194
+ async function runGated(context, input) {
195
+ const { client, stderr } = context;
196
+ const { capabilityId } = input;
197
+ const idempotencyPath = context.idempotencyPath ?? `${context.turnsPath}.attempts`;
198
+ const body = { ...input.body };
199
+ let fetched = null;
200
+ const contract = async () => {
201
+ fetched ??= await client.getTool(capabilityId);
202
+ return fetched;
203
+ };
204
+ let warnedAboutTurnCache = false;
205
+ const warnTurnCache = () => {
206
+ if (warnedAboutTurnCache)
207
+ return;
208
+ warnedAboutTurnCache = true;
209
+ stderr.write('Warning: Dreamstate turn cache unavailable; continuing without turn reuse.\n');
210
+ };
211
+ // Flag-built params are converted before anything is fingerprinted, priced or sent.
212
+ if (input.coerceParams || input.adjustParams) {
213
+ const capability = capabilityRecord(await contract());
214
+ let params = record(body.params);
215
+ if (params && input.coerceParams) {
216
+ try {
217
+ (0, schemaCoercion_1.assertParamsMatchSchema)(params, capability?.input_json_schema);
218
+ }
219
+ catch (error) {
220
+ throw new errors_1.CliUsageError(error instanceof Error ? error.message : 'live schema validation failed');
221
+ }
222
+ params = (0, schemaCoercion_1.coerceParamsToSchema)(params, capability?.input_json_schema).params;
223
+ }
224
+ if (params && input.adjustParams)
225
+ params = input.adjustParams(params, capability);
226
+ if (params)
227
+ body.params = params;
228
+ }
229
+ // A paid live run needs the quote_hash of a dry run of the same input,
230
+ // and a quote is bound to the turn that asked for it. So a turn is reused
231
+ // only while a quote is pending, together with the quote its dry run
232
+ // returned, and forgotten once the paid run is admitted.
233
+ const fingerprint = planFingerprint(body);
234
+ const cached = input.turnToken
235
+ ? null
236
+ : await (0, turnCache_1.reusableTurn)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, fingerprint, Date.now(), warnTurnCache);
237
+ const pending = cached?.token ?? null;
238
+ const live = body.mode === 'live';
239
+ const inlineRead = !input.turnToken && !pending && !input.idempotencyKey && omitsToolTurnForInlineTableRead(capabilityId);
240
+ if (!input.turnToken && !pending && !inlineRead)
241
+ await contract();
242
+ const refusal = fetched ? cliSurfaceRefusal(capabilityId, fetched) : null;
243
+ if (refusal)
244
+ throw refusal;
245
+ let turnToken = input.turnToken ?? pending ?? (inlineRead ? undefined : toolTurnToken(fetched));
246
+ // Only a quote the caller named or a dry run of this exact input returned.
247
+ // The server's refusal also shows a price, but that one is information, not consent.
248
+ let quoteHash = typeof body.quote_hash === 'string'
249
+ ? body.quote_hash
250
+ : live ? cached?.quote_hash ?? undefined : undefined;
251
+ const explicitKey = parameterIdempotencyKey(body) ?? input.idempotencyKey;
252
+ let recoveredInlineRead = false;
253
+ let ranDryRun = false;
254
+ let sentKey = null;
255
+ const requestBody = () => ({
256
+ ...body,
257
+ ...(turnToken ? { tool_turn_token: turnToken } : {}),
258
+ ...(live && quoteHash ? { quote_hash: quoteHash } : {}),
259
+ });
260
+ // A write is replay-safe on live runs; a rehearsal has no effect to replay.
261
+ let attemptIdentity = null;
262
+ const keyFor = async (sent) => {
263
+ if (explicitKey)
264
+ return explicitKey;
265
+ if (sent.mode === 'dry_run' || sent.mode === 'smoke_test')
266
+ return null;
267
+ const identity = { scope: capabilityId, workspace: context.workspace, body: sent };
268
+ const known = capabilityIdempotencyKey(fetched, undefined, identity);
269
+ if (known) {
270
+ attemptIdentity = identity;
271
+ return (0, idempotency_1.pendingIdempotencyKey)(idempotencyPath, identity);
272
+ }
273
+ // A cached turn exists only because an earlier dry run of this input priced a
274
+ // paid write, so the live rerun is a write even though no contract was fetched.
275
+ return !fetched && !input.turnToken && live && pending
276
+ ? await (0, idempotency_1.pendingIdempotencyKey)(idempotencyPath, (attemptIdentity = { scope: capabilityId, workspace: context.workspace, body: sent }))
277
+ : null;
278
+ };
279
+ const run = async () => {
280
+ const sent = requestBody();
281
+ sentKey = await keyFor(sent);
282
+ return client.runCapability(capabilityId, sent, sentKey);
283
+ };
284
+ let result;
285
+ try {
286
+ try {
287
+ result = await run();
288
+ }
289
+ catch (error) {
290
+ // The inline skip is only valid while the server still treats this
291
+ // capability as a free sync read. capability_not_fetched means the
292
+ // fetch-first fence is on, so retry once on the old get-then-run path.
293
+ if (inlineRead && !recoveredInlineRead && code(error) === 'capability_not_fetched') {
294
+ recoveredInlineRead = true;
295
+ fetched = await client.getTool(capabilityId);
296
+ turnToken = toolTurnToken(fetched);
297
+ result = await run();
298
+ }
299
+ else if (live && code(error) === 'quote_required' && !quoteHash && turnToken) {
300
+ // A live paid run with no quote behind it. The refusal's own quote is
301
+ // only a price and is never replayed, so price the same input with a
302
+ // real dry run (free, never calls a paid provider), show the person
303
+ // its rows and cost, and run live on that quote. Above the size or
304
+ // dollar threshold the server still refuses until --approved.
305
+ const dry = await client.runCapability(capabilityId, {
306
+ ...body,
307
+ mode: 'dry_run',
308
+ tool_turn_token: turnToken,
309
+ }, null);
310
+ const quote = quoteFromResult(dry);
311
+ if (!quote)
312
+ throw error;
313
+ // The no-quote call was definitively refused before execution. Its
314
+ // pending attempt must not survive into the separately quoted call.
315
+ if (attemptIdentity) {
316
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
317
+ attemptIdentity = null;
318
+ }
319
+ ranDryRun = true;
320
+ quoteHash = quote.quote_hash;
321
+ stderr.write(`${quoteSummary(capabilityId, body, quote)}\n`);
322
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, { quote_hash: quote.quote_hash, fingerprint });
323
+ result = await run();
324
+ }
325
+ else {
326
+ throw error;
327
+ }
328
+ }
329
+ }
330
+ catch (error) {
331
+ // Keep the turn and the quote across a refusal that a person can fix,
332
+ // so the rerun (with --approved, or a stated max_rows) reuses them.
333
+ if (!input.turnToken && turnToken && (code(error) === 'quote_required' || code(error) === 'approval_required')) {
334
+ if (code(error) === 'approval_required') {
335
+ stderr.write('This run needs a person\'s approval. Show them the rows and cost above; when they agree, rerun the same command with --approved.\n');
336
+ }
337
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quoteHash ? { quote_hash: quoteHash, fingerprint } : undefined);
338
+ }
339
+ // Only an unknown or transient result may resume an attempt. A server
340
+ // refusal with retryable:false is terminal and must receive a new key.
341
+ const transient = error instanceof client_1.CliError && (error.code === 'request_timeout' || error.code === 'network_error' || error.retryable === true || error.status >= 500);
342
+ if (attemptIdentity && !transient)
343
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
344
+ throw error;
345
+ }
346
+ if (!input.turnToken) {
347
+ if (body.mode === 'dry_run' && turnToken) {
348
+ const quote = quoteFromResult(result);
349
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quote ? { quote_hash: quote.quote_hash, fingerprint } : undefined);
350
+ }
351
+ else if (pending || ranDryRun)
352
+ await (0, turnCache_1.forgetTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, warnTurnCache);
353
+ }
354
+ let timedOut = false;
355
+ if (!input.noWait && !(0, runWait_1.columnRunReturnsRunId)(capabilityId)) {
356
+ const waited = await (0, runWait_1.waitForRun)(client, result, input.waitTimeout, context.sleep);
357
+ result = waited.value;
358
+ timedOut = waited.timedOut;
359
+ }
360
+ // A run still active after waiting has unknown outcome; retain its key for
361
+ // the read-back/retry rule. A terminal response completes this attempt.
362
+ if (attemptIdentity && !timedOut)
363
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
364
+ return { value: withIdempotencyMeta(result, explicitKey ? null : sentKey), timedOut, idempotencyKey: sentKey, turnToken: turnToken ?? null };
365
+ }
366
+ /** A read the CLI needs before it can form a write, run through the same gate as any other run. */
367
+ async function readCapability(context, capabilityId, params, waitTimeout) {
368
+ const outcome = await runGated(context, { capabilityId, body: { params }, waitTimeout });
369
+ return outcome.value;
370
+ }
@@ -0,0 +1,38 @@
1
+ /** A key-sorted JSON string, so two spellings of one input fingerprint alike. */
2
+ export declare function stableJson(value: unknown): string;
3
+ export interface IdempotencyIdentity {
4
+ /** What the key is for: a capability id, or a control such as runs.cancel. */
5
+ scope: string;
6
+ /** Workspace the command acts in; the same body in another workspace is another command. */
7
+ workspace?: string | null;
8
+ /** The request body that will be sent (params, mode, max_credits, quote_hash, approved ...). */
9
+ body: unknown;
10
+ }
11
+ export declare function pendingIdempotencyPath(credentialsPath: string): string;
12
+ /**
13
+ * Deterministic idempotency key for one logical command.
14
+ *
15
+ * A random key per invocation made a re-run after a timeout a brand new
16
+ * operation: the server could not replay the first attempt, so a write whose
17
+ * response was lost ran twice. The key is now a digest of the capability, the
18
+ * canonical request body and the workspace, so running the same command again
19
+ * presents the same key and the server replays. A key the caller names with
20
+ * --idempotency-key is used as given and never reaches this function.
21
+ *
22
+ * Because the whole body is hashed, a changed input, ceiling, mode, quote or
23
+ * approval flag is a different command with a different key. That is the same
24
+ * rule the server applies when it refuses a reused key with a different body
25
+ * (idempotency_key_reuse), so a deliberate change can never collide with an
26
+ * earlier attempt.
27
+ */
28
+ export declare function idempotencyDigest(identity: IdempotencyIdentity): string;
29
+ /**
30
+ * Request digest, not a durable command key. A terminal attempt removes its
31
+ * entry; only an interrupted or transient attempt is resumed with this key.
32
+ */
33
+ export declare function deterministicIdempotencyKey(identity: IdempotencyIdentity): string;
34
+ /** Gets a fresh attempt key, or resumes only a pending attempt for this digest. */
35
+ export declare function pendingIdempotencyKey(path: string, identity: IdempotencyIdentity): Promise<string>;
36
+ /** A success or definitive refusal finishes an attempt; a transient outcome keeps it resumable. */
37
+ export declare function finishIdempotencyAttempt(path: string, identity: IdempotencyIdentity): Promise<void>;
38
+ //# sourceMappingURL=idempotency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idempotency.d.ts","sourceRoot":"","sources":["../src/idempotency.ts"],"names":[],"mappings":"AAIA,iFAAiF;AACjF,wBAAgB,UAAU,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAOjD;AASD,MAAM,WAAW,mBAAmB;IAClC,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,gGAAgG;IAChG,IAAI,EAAE,OAAO,CAAC;CACf;AAED,wBAAgB,sBAAsB,CAAC,eAAe,EAAE,MAAM,GAAG,MAAM,CAEtE;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,mBAAmB,GAAG,MAAM,CAavE;AAED;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,QAAQ,EAAE,mBAAmB,GAAG,MAAM,CAEjF;AAqBD,mFAAmF;AACnF,wBAAsB,qBAAqB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,CAAC,CASxG;AAED,mGAAmG;AACnG,wBAAsB,wBAAwB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC,CAMzG"}