@trydreamstate/cli 1.3.2 → 1.3.4

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 (91) hide show
  1. package/README.md +36 -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 +3 -1
  8. package/agent-plugin/skills/dreamstate/SKILL.md +15 -5
  9. package/agent-plugin/skills/first-win/SKILL.md +2 -2
  10. package/agent-plugin/skills/inbox-replies/SKILL.md +3 -1
  11. package/agent-plugin/skills/knowledge/SKILL.md +3 -5
  12. package/agent-plugin/skills/linkedin-posts/SKILL.md +3 -1
  13. package/agent-plugin/skills/linkedin-warm-signals/SKILL.md +2 -2
  14. package/agent-plugin/skills/list-building/SKILL.md +5 -10
  15. package/agent-plugin/skills/list-building/column-authoring.md +1 -1
  16. package/agent-plugin/skills/list-building/sourcing.md +2 -2
  17. package/agent-plugin/skills/outreach/SKILL.md +2 -2
  18. package/agent-plugin/skills/outreach/steps-and-timing.md +1 -1
  19. package/agent-plugin/skills/plays/SKILL.md +3 -3
  20. package/agent-plugin/skills/reddit-posts/SKILL.md +3 -1
  21. package/agent-plugin/skills/skill-authoring/SKILL.md +3 -1
  22. package/agent-plugin/skills/social/SKILL.md +4 -6
  23. package/agent-plugin/skills/visibility/SKILL.md +2 -6
  24. package/agent-plugin/skills/workflows/SKILL.md +3 -3
  25. package/agent-plugin/skills/workflows/lifecycle-and-enrollment.md +1 -1
  26. package/agent-plugin/skills/worksheet-tidy/SKILL.md +3 -1
  27. package/agent-plugin/skills/writing/SKILL.md +3 -5
  28. package/agent-plugin/skills/x-posts/SKILL.md +3 -1
  29. package/dist/agentGuidance.generated.json +35 -0
  30. package/dist/capabilityPosture.d.ts +12 -0
  31. package/dist/capabilityPosture.d.ts.map +1 -0
  32. package/dist/capabilityPosture.js +39 -0
  33. package/dist/client.d.ts +33 -2
  34. package/dist/client.d.ts.map +1 -1
  35. package/dist/client.js +155 -64
  36. package/dist/commandTable.d.ts +26 -0
  37. package/dist/commandTable.d.ts.map +1 -0
  38. package/dist/commandTable.js +54 -0
  39. package/dist/commands/sequences.d.ts +3 -2
  40. package/dist/commands/sequences.d.ts.map +1 -1
  41. package/dist/commands/sequences.js +17 -34
  42. package/dist/commands/shared.d.ts +4 -26
  43. package/dist/commands/shared.d.ts.map +1 -1
  44. package/dist/commands/shared.js +136 -158
  45. package/dist/commands/workflows.d.ts +3 -2
  46. package/dist/commands/workflows.d.ts.map +1 -1
  47. package/dist/commands/workflows.js +17 -39
  48. package/dist/errors.d.ts +74 -0
  49. package/dist/errors.d.ts.map +1 -0
  50. package/dist/errors.js +84 -0
  51. package/dist/gatedRun.d.ts +79 -0
  52. package/dist/gatedRun.d.ts.map +1 -0
  53. package/dist/gatedRun.js +346 -0
  54. package/dist/generated/packVerbs.generated.json +492 -0
  55. package/dist/generated/runContract.generated.d.ts +4 -0
  56. package/dist/generated/runContract.generated.d.ts.map +1 -0
  57. package/dist/generated/runContract.generated.js +7 -0
  58. package/dist/idempotency.d.ts +38 -0
  59. package/dist/idempotency.d.ts.map +1 -0
  60. package/dist/idempotency.js +106 -0
  61. package/dist/index.d.ts +22 -35
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +405 -378
  64. package/dist/registryManifest.d.ts +6 -0
  65. package/dist/registryManifest.d.ts.map +1 -0
  66. package/dist/registryManifest.js +66 -0
  67. package/dist/runContract.d.ts +10 -0
  68. package/dist/runContract.d.ts.map +1 -0
  69. package/dist/runContract.js +13 -0
  70. package/dist/runWait.d.ts +2 -2
  71. package/dist/runWait.d.ts.map +1 -1
  72. package/dist/runWait.js +3 -14
  73. package/dist/schemaCoercion.d.ts +20 -0
  74. package/dist/schemaCoercion.d.ts.map +1 -0
  75. package/dist/schemaCoercion.js +123 -0
  76. package/dist/skillInstaller.d.ts +40 -0
  77. package/dist/skillInstaller.d.ts.map +1 -1
  78. package/dist/skillInstaller.js +108 -9
  79. package/dist/tableFilePreview.d.ts +14 -0
  80. package/dist/tableFilePreview.d.ts.map +1 -1
  81. package/dist/tableFilePreview.js +19 -8
  82. package/dist/tableFileReader.d.ts +26 -0
  83. package/dist/tableFileReader.d.ts.map +1 -0
  84. package/dist/tableFileReader.js +108 -0
  85. package/dist/tablesImport.d.ts +57 -0
  86. package/dist/tablesImport.d.ts.map +1 -0
  87. package/dist/tablesImport.js +263 -0
  88. package/dist/turnCache.d.ts +14 -1
  89. package/dist/turnCache.d.ts.map +1 -1
  90. package/dist/turnCache.js +25 -8
  91. package/package.json +6 -3
package/dist/errors.js ADDED
@@ -0,0 +1,84 @@
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.isApprovalRepair = isApprovalRepair;
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 repair.kind ask_user (a person must decide),
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_web_url: 'The API returned a web_url that is not a safe http(s) URL.',
54
+ web_url_unavailable: '--open was given but the response carries no web_url.',
55
+ plugin_install_failed: 'dreamstate install could not install the plugin for any agent.',
56
+ skills_authentication_required: 'skills install needs a stored login.',
57
+ };
58
+ /** Recovery metadata for failures raised by this CLI, rather than the API. */
59
+ exports.CLI_LOCAL_ERROR_REPAIRS = {
60
+ network_error: { kind: 'wait', hint: 'Retry the unchanged request when network connectivity is available.' },
61
+ request_timeout: { kind: 'ask_user', hint: 'Read the target back first. If absent, repeat once with the same idempotency key; otherwise ask the person.' },
62
+ invalid_response: { kind: 'server_defect', hint: 'The API response was not a Dreamstate envelope; report the trace id if one was returned.' },
63
+ invalid_web_url: { kind: 'server_defect', hint: 'The API returned an invalid web URL; report the response trace id.' },
64
+ web_url_unavailable: { kind: 'not_possible', hint: 'This result has no web URL to open.' },
65
+ plugin_install_failed: { kind: 'ask_user', hint: 'Install a supported agent, then retry plugin installation.' },
66
+ skills_authentication_required: { kind: 'ask_user', hint: 'Sign in with dreamstate login, then retry.' },
67
+ };
68
+ function isApprovalRepair(error) {
69
+ if (!error || typeof error !== 'object')
70
+ return false;
71
+ const repair = error.repair;
72
+ if (!repair || typeof repair !== 'object')
73
+ return false;
74
+ const kind = repair.kind;
75
+ return kind === 'ask_user';
76
+ }
77
+ /**
78
+ * Refusals the CLI decides locally, before running anything, that are not usage
79
+ * errors: the capability is not published for the CLI surface.
80
+ */
81
+ const LOCAL_REFUSAL_CODES = new Set(['unsupported_capability']);
82
+ function isLocalRefusalCode(code) {
83
+ return Boolean(code) && LOCAL_REFUSAL_CODES.has(code);
84
+ }
@@ -0,0 +1,79 @@
1
+ import { CliError, type DreamstateClient } from './client';
2
+ import { type IdempotencyIdentity } from './idempotency';
3
+ export declare function code(error: unknown): string | undefined;
4
+ /** The capability record inside a tools get response, or null when the shape is unknown. */
5
+ export declare function capabilityRecord(fetched: unknown): Record<string, unknown> | null;
6
+ /**
7
+ * Header value, or null when the request must not carry Idempotency-Key.
8
+ * Any key forces a read onto the durable run path (isInlineReadEligible).
9
+ * Send one only for an explicit key, or when the tools get contract says
10
+ * mutates or idempotency_required. Missing flags omit the key: a read whose
11
+ * contract does not set mutates must not be treated as a write.
12
+ *
13
+ * A write's key is scoped to an attempt (idempotency.ts). A pending interrupted
14
+ * attempt reuses its key; a terminal attempt gets a fresh key on a later command.
15
+ */
16
+ export declare function capabilityIdempotencyKey(fetched: unknown, explicit: string | undefined, identity: IdempotencyIdentity): string | null;
17
+ export declare function parameterIdempotencyKey(body: Record<string, unknown>): string | undefined;
18
+ /**
19
+ * Whether the capability is published for the CLI surface. The tools get
20
+ * contract lists the CLI commands the catalog advertises (cli_commands); an
21
+ * empty list means the capability is not on this surface. This reads the
22
+ * field the contract carries today; when the shared surface-visibility
23
+ * function (contract C7) reaches the contract this is the one place to
24
+ * call it. A contract that says nothing about the CLI is allowed, because a
25
+ * missing field is not a refusal.
26
+ */
27
+ export declare function cliSurfaceRefusal(capabilityId: string, fetched: unknown): CliError | null;
28
+ /**
29
+ * What a quote prices: the params plus the ceiling. Keys that say how a call is
30
+ * made, not what it does, are left out, like the server's own plan digest.
31
+ */
32
+ export declare function planFingerprint(body: Record<string, unknown>): string;
33
+ export interface GatedRunContext {
34
+ client: DreamstateClient;
35
+ baseUrl: string;
36
+ /** apiKey plus workspace: a turn belongs to one workspace, so it is part of the cache identity. */
37
+ turnKey: string;
38
+ turnsPath: string;
39
+ /** Pending live-attempt keys, stored next to CLI credentials. */
40
+ idempotencyPath?: string;
41
+ workspace?: string;
42
+ stderr: NodeJS.WritableStream;
43
+ sleep: (milliseconds: number) => Promise<void>;
44
+ }
45
+ export interface GatedRunInput {
46
+ capabilityId: string;
47
+ /** Door body: params plus mode, approved, max_credits, quote_hash, approval_id ... */
48
+ body: Record<string, unknown>;
49
+ /** --turn-token: a token the caller already holds from tools get. */
50
+ turnToken?: string;
51
+ /** --idempotency-key: used as given. Without it a write gets a deterministic key. */
52
+ idempotencyKey?: string;
53
+ waitTimeout: number;
54
+ noWait?: boolean;
55
+ /**
56
+ * The params came from shell flags (pack verbs), so each string value is
57
+ * converted to the type the capability's live input schema declares.
58
+ */
59
+ coerceParams?: boolean;
60
+ /**
61
+ * Last edit of the params once the live capability contract is known. Used by
62
+ * pack verbs to bridge a capability that still declares a gating input inside
63
+ * its params (contract C6 keeps those at the door): the verb copies the door
64
+ * value into params only while the contract's input schema still lists it.
65
+ */
66
+ adjustParams?: (params: Record<string, unknown>, capability: Record<string, unknown> | null) => Record<string, unknown>;
67
+ }
68
+ export interface GatedRunOutcome {
69
+ value: unknown;
70
+ timedOut: boolean;
71
+ /** The Idempotency-Key the run was sent with, or null when none applied. */
72
+ idempotencyKey: string | null;
73
+ /** The tool_turn_token the run used, so a caller making many runs of one capability can reuse it. */
74
+ turnToken: string | null;
75
+ }
76
+ export declare function runGated(context: GatedRunContext, input: GatedRunInput): Promise<GatedRunOutcome>;
77
+ /** A read the CLI needs before it can form a write, run through the same gate as any other run. */
78
+ export declare function readCapability(context: GatedRunContext, capabilityId: string, params: Record<string, unknown>, waitTimeout: number): Promise<unknown>;
79
+ //# sourceMappingURL=gatedRun.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gatedRun.d.ts","sourceRoot":"","sources":["../src/gatedRun.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,QAAQ,EAA0B,KAAK,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAEnF,OAAO,EAA4F,KAAK,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAoBnJ,wBAAgB,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAIvD;AAUD,4FAA4F;AAC5F,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAKjF;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,mBAAmB,GAAG,MAAM,GAAG,IAAI,CAKrI;AAED,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,GAAG,SAAS,CAKzF;AAED;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAazF;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAMrE;AAoCD,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,gBAAgB,CAAC;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,mGAAmG;IACnG,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,MAAM,CAAC,cAAc,CAAC;IAC9B,KAAK,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,sFAAsF;IACtF,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC9B,qEAAqE;IACrE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qFAAqF;IACrF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;OAGG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACzH;AAED,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,OAAO,CAAC;IAClB,4EAA4E;IAC5E,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,qGAAqG;IACrG,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AASD,wBAAsB,QAAQ,CAAC,OAAO,EAAE,eAAe,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,eAAe,CAAC,CAsKvG;AAED,mGAAmG;AACnG,wBAAsB,cAAc,CAClC,OAAO,EAAE,eAAe,EACxB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,OAAO,CAAC,CAGlB"}
@@ -0,0 +1,346 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.code = code;
4
+ exports.capabilityRecord = capabilityRecord;
5
+ exports.capabilityIdempotencyKey = capabilityIdempotencyKey;
6
+ exports.parameterIdempotencyKey = parameterIdempotencyKey;
7
+ exports.cliSurfaceRefusal = cliSurfaceRefusal;
8
+ exports.planFingerprint = planFingerprint;
9
+ exports.runGated = runGated;
10
+ exports.readCapability = readCapability;
11
+ const node_crypto_1 = require("node:crypto");
12
+ const client_1 = require("./client");
13
+ const errors_1 = require("./errors");
14
+ const idempotency_1 = require("./idempotency");
15
+ const schemaCoercion_1 = require("./schemaCoercion");
16
+ const runWait_1 = require("./runWait");
17
+ const turnCache_1 = require("./turnCache");
18
+ const capabilityPosture_1 = require("./capabilityPosture");
19
+ const registryManifest_1 = require("./registryManifest");
20
+ const runContract_1 = require("./runContract");
21
+ /**
22
+ * The one path every capability run takes: `tools run` and every pack verb
23
+ * (`sequences ...`, `workflows ...`) call runGated. Gating lives here and only
24
+ * here (contract C6): mode, max_credits, quote_hash, approved, approval_id and
25
+ * the idempotency key are door inputs, handled once, so a verb can never be a
26
+ * quieter way to run what `tools run` would stop.
27
+ */
28
+ function record(value) {
29
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
30
+ }
31
+ function code(error) {
32
+ return error && typeof error === 'object' && typeof error.code === 'string'
33
+ ? error.code
34
+ : undefined;
35
+ }
36
+ function optionalToolTurnToken(value) {
37
+ const root = value && typeof value === 'object' ? value : {};
38
+ const data = root.data && typeof root.data === 'object' ? root.data : {};
39
+ return typeof data.tool_turn_token === 'string' && data.tool_turn_token.trim()
40
+ ? data.tool_turn_token
41
+ : undefined;
42
+ }
43
+ /** The capability record inside a tools get response, or null when the shape is unknown. */
44
+ function capabilityRecord(fetched) {
45
+ const root = record(fetched);
46
+ if (!root)
47
+ return null;
48
+ const data = record(root.data);
49
+ return record(data?.capability) ?? record(root.capability);
50
+ }
51
+ function readIdempotencyContract(fetched) {
52
+ if (!fetched || typeof fetched !== 'object' || Array.isArray(fetched))
53
+ return null;
54
+ const root = fetched;
55
+ const data = record(root.data);
56
+ const candidates = [data?.capability, root.capability, data, root];
57
+ for (const candidate of candidates) {
58
+ if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
59
+ continue;
60
+ if ('mutates' in candidate || 'idempotency_required' in candidate) {
61
+ return candidate;
62
+ }
63
+ }
64
+ return null;
65
+ }
66
+ /**
67
+ * Header value, or null when the request must not carry Idempotency-Key.
68
+ * Any key forces a read onto the durable run path (isInlineReadEligible).
69
+ * Send one only for an explicit key, or when the tools get contract says
70
+ * mutates or idempotency_required. Missing flags omit the key: a read whose
71
+ * contract does not set mutates must not be treated as a write.
72
+ *
73
+ * A write's key is scoped to an attempt (idempotency.ts). A pending interrupted
74
+ * attempt reuses its key; a terminal attempt gets a fresh key on a later command.
75
+ */
76
+ function capabilityIdempotencyKey(fetched, explicit, identity) {
77
+ if (typeof explicit === 'string' && explicit.length > 0)
78
+ return explicit;
79
+ const contract = readIdempotencyContract(fetched);
80
+ if (contract?.mutates === true || contract?.idempotency_required === true)
81
+ return (0, idempotency_1.deterministicIdempotencyKey)(identity);
82
+ return null;
83
+ }
84
+ function parameterIdempotencyKey(body) {
85
+ const params = body.params;
86
+ if (!params || typeof params !== 'object' || Array.isArray(params))
87
+ return undefined;
88
+ const key = params.idempotency_key;
89
+ return typeof key === 'string' && key.length > 0 ? key : undefined;
90
+ }
91
+ /**
92
+ * Whether the capability is published for the CLI surface. The tools get
93
+ * contract lists the CLI commands the catalog advertises (cli_commands); an
94
+ * empty list means the capability is not on this surface. This reads the
95
+ * field the contract carries today; when the shared surface-visibility
96
+ * function (contract C7) reaches the contract this is the one place to
97
+ * call it. A contract that says nothing about the CLI is allowed, because a
98
+ * missing field is not a refusal.
99
+ */
100
+ function cliSurfaceRefusal(capabilityId, fetched) {
101
+ const capability = capabilityRecord(fetched);
102
+ if (!capability)
103
+ return null;
104
+ const commands = capability.cli_commands;
105
+ const surfaces = record(capability.surfaces);
106
+ const absent = Array.isArray(commands) ? commands.length === 0 : surfaces?.cli === false;
107
+ if (!absent)
108
+ return null;
109
+ 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 });
110
+ }
111
+ /**
112
+ * What a quote prices: the params plus the ceiling. Keys that say how a call is
113
+ * made, not what it does, are left out, like the server's own plan digest.
114
+ */
115
+ function planFingerprint(body) {
116
+ const params = body.params && typeof body.params === 'object' && !Array.isArray(body.params)
117
+ ? { ...body.params }
118
+ : {};
119
+ for (const key of ['quote_hash', 'plan_digest', 'estimate_digest', 'approved', 'max_credits', 'mode', 'dry_run', 'idempotency_key', 'tool_turn_token'])
120
+ delete params[key];
121
+ return (0, node_crypto_1.createHash)('sha256').update((0, idempotency_1.stableJson)({ params, max_credits: body.max_credits ?? null })).digest('hex');
122
+ }
123
+ /** The quote a dry run returned, wherever the envelope nests its outputs. */
124
+ function quoteFromResult(value, depth = 0) {
125
+ if (!value || typeof value !== 'object' || depth > 6)
126
+ return null;
127
+ if (Array.isArray(value)) {
128
+ for (const entry of value) {
129
+ const found = quoteFromResult(entry, depth + 1);
130
+ if (found)
131
+ return found;
132
+ }
133
+ return null;
134
+ }
135
+ const entry = value;
136
+ if (typeof entry.quote_hash === 'string' && runContract_1.QUOTE_HASH_PATTERN.test(entry.quote_hash)) {
137
+ const estimate = entry.estimate && typeof entry.estimate === 'object' ? entry.estimate : null;
138
+ return {
139
+ quote_hash: entry.quote_hash,
140
+ maximum_credits: typeof estimate?.maximum_credits === 'number' ? estimate.maximum_credits : null,
141
+ };
142
+ }
143
+ for (const key of ['data', 'run', 'outputs', 'steps']) {
144
+ const found = quoteFromResult(entry[key], depth + 1);
145
+ if (found)
146
+ return found;
147
+ }
148
+ return null;
149
+ }
150
+ function quoteSummary(capabilityId, body, quote) {
151
+ const params = body.params && typeof body.params === 'object' ? body.params : {};
152
+ const rows = typeof params.max_rows === 'number' ? `${params.max_rows} rows` : 'rows not stated';
153
+ const credits = quote.maximum_credits === null ? 'credits not stated' : `up to ${quote.maximum_credits} credits`;
154
+ return `Dry run of ${capabilityId} priced: ${rows}, ${credits}. Running live on that quote.`;
155
+ }
156
+ /** Adds the idempotency key to the printed meta, so a user can see which key to re-run with. */
157
+ function withIdempotencyMeta(value, key) {
158
+ const envelope = record(value);
159
+ if (!envelope || !key)
160
+ return value;
161
+ return { ...envelope, meta: { ...(record(envelope.meta) ?? {}), idempotency_key: key } };
162
+ }
163
+ async function runGated(context, input) {
164
+ const { client, stderr } = context;
165
+ const { capabilityId } = input;
166
+ const idempotencyPath = context.idempotencyPath ?? `${context.turnsPath}.attempts`;
167
+ const body = { ...input.body };
168
+ let fetched = null;
169
+ const contract = async () => {
170
+ fetched ??= await client.getTool(capabilityId);
171
+ return fetched;
172
+ };
173
+ let warnedAboutTurnCache = false;
174
+ const warnTurnCache = () => {
175
+ if (warnedAboutTurnCache)
176
+ return;
177
+ warnedAboutTurnCache = true;
178
+ stderr.write('Warning: Dreamstate turn cache unavailable; continuing without turn reuse.\n');
179
+ };
180
+ // Flag-built params are converted before anything is fingerprinted, priced or sent.
181
+ if (input.coerceParams || input.adjustParams) {
182
+ const capability = capabilityRecord(await contract());
183
+ let params = record(body.params);
184
+ if (params && input.coerceParams) {
185
+ try {
186
+ (0, schemaCoercion_1.assertParamsMatchSchema)(params, capability?.input_json_schema);
187
+ }
188
+ catch (error) {
189
+ throw new errors_1.CliUsageError(error instanceof Error ? error.message : 'live schema validation failed');
190
+ }
191
+ params = (0, schemaCoercion_1.coerceParamsToSchema)(params, capability?.input_json_schema).params;
192
+ }
193
+ if (params && input.adjustParams)
194
+ params = input.adjustParams(params, capability);
195
+ if (params)
196
+ body.params = params;
197
+ }
198
+ // A paid live run needs the quote_hash of a dry run of the same input,
199
+ // and a quote is bound to the turn that asked for it. So a turn is reused
200
+ // only while a quote is pending, together with the quote its dry run
201
+ // returned, and forgotten once the paid run is admitted.
202
+ const explicitKey = parameterIdempotencyKey(body) ?? input.idempotencyKey;
203
+ const fingerprint = planFingerprint(body);
204
+ const cached = input.turnToken
205
+ ? null
206
+ : await (0, turnCache_1.reusableTurn)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, fingerprint, Date.now(), warnTurnCache);
207
+ const pending = cached?.token ?? null;
208
+ const live = body.mode === 'live';
209
+ // --turn-token is the contract. Do not fetch tools get to invent one.
210
+ // A cached inline read also skips the fetch until registry_hash changes.
211
+ const cachedPosture = input.turnToken ? null : (0, registryManifest_1.lookupPosture)(capabilityId);
212
+ const inlineFromCache = !input.turnToken && !pending && !explicitKey && cachedPosture?.inlineRead === true;
213
+ if (!input.turnToken && !pending && !inlineFromCache)
214
+ await contract();
215
+ const posture = fetched ? (0, capabilityPosture_1.postureFromContract)(fetched) : cachedPosture;
216
+ if (fetched && posture)
217
+ (0, registryManifest_1.rememberPosture)(capabilityId, posture);
218
+ const refusal = fetched ? cliSurfaceRefusal(capabilityId, fetched) : null;
219
+ if (refusal)
220
+ throw refusal;
221
+ const inlineRead = !input.turnToken && !pending && !explicitKey && posture?.inlineRead === true;
222
+ let turnToken = input.turnToken ?? pending ?? (inlineRead ? undefined : optionalToolTurnToken(fetched));
223
+ // Only a quote the caller named or a dry run of this exact input returned.
224
+ // The server's refusal also shows a price, but that one is information, not consent.
225
+ let quoteHash = typeof body.quote_hash === 'string'
226
+ ? body.quote_hash
227
+ : live ? cached?.quote_hash ?? undefined : undefined;
228
+ let ranDryRun = false;
229
+ let sentKey = null;
230
+ const requestBody = () => ({
231
+ ...body,
232
+ ...(turnToken ? { tool_turn_token: turnToken } : {}),
233
+ ...(live && quoteHash ? { quote_hash: quoteHash } : {}),
234
+ });
235
+ // A write is replay-safe on live runs; a rehearsal has no effect to replay.
236
+ let attemptIdentity = null;
237
+ const keyFor = async (sent) => {
238
+ if (explicitKey)
239
+ return explicitKey;
240
+ if (sent.mode === 'dry_run' || sent.mode === 'smoke_test')
241
+ return null;
242
+ const identity = { scope: capabilityId, workspace: context.workspace, body: sent };
243
+ const known = capabilityIdempotencyKey(fetched, undefined, identity);
244
+ if (known) {
245
+ attemptIdentity = identity;
246
+ return (0, idempotency_1.pendingIdempotencyKey)(idempotencyPath, identity);
247
+ }
248
+ // A cached turn exists only because an earlier dry run of this input priced a
249
+ // paid write, so the live rerun is a write even though no contract was fetched.
250
+ return !fetched && !input.turnToken && live && pending
251
+ ? await (0, idempotency_1.pendingIdempotencyKey)(idempotencyPath, (attemptIdentity = { scope: capabilityId, workspace: context.workspace, body: sent }))
252
+ : null;
253
+ };
254
+ const run = async () => {
255
+ const sent = requestBody();
256
+ sentKey = await keyFor(sent);
257
+ return client.runCapability(capabilityId, sent, sentKey);
258
+ };
259
+ let result;
260
+ try {
261
+ try {
262
+ result = await run();
263
+ }
264
+ catch (error) {
265
+ if (!input.turnToken && code(error) === 'capability_not_fetched') {
266
+ // Capability posture can change between tools get and run. Refresh it
267
+ // once rather than maintaining a client-side list of token-free reads.
268
+ fetched = await client.getTool(capabilityId);
269
+ const refreshed = (0, capabilityPosture_1.postureFromContract)(fetched);
270
+ // The server just required a turn token, so this id is not an inline read.
271
+ (0, registryManifest_1.rememberPosture)(capabilityId, { ...refreshed, inlineRead: false });
272
+ turnToken = optionalToolTurnToken(fetched);
273
+ result = await run();
274
+ }
275
+ else if (live && code(error) === 'quote_required' && !quoteHash && turnToken) {
276
+ // A live paid run with no quote behind it. The refusal's own quote is
277
+ // only a price and is never replayed, so price the same input with a
278
+ // real dry run (free, never calls a paid provider), show the person
279
+ // its rows and cost, and run live on that quote. Above the size or
280
+ // dollar threshold the server still refuses until --approved.
281
+ const dry = await client.runCapability(capabilityId, {
282
+ ...body,
283
+ mode: 'dry_run',
284
+ tool_turn_token: turnToken,
285
+ }, null);
286
+ const quote = quoteFromResult(dry);
287
+ if (!quote)
288
+ throw error;
289
+ // The no-quote call was definitively refused before execution. Its
290
+ // pending attempt must not survive into the separately quoted call.
291
+ if (attemptIdentity) {
292
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
293
+ attemptIdentity = null;
294
+ }
295
+ ranDryRun = true;
296
+ quoteHash = quote.quote_hash;
297
+ stderr.write(`${quoteSummary(capabilityId, body, quote)}\n`);
298
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, { quote_hash: quote.quote_hash, fingerprint });
299
+ result = await run();
300
+ }
301
+ else {
302
+ throw error;
303
+ }
304
+ }
305
+ }
306
+ catch (error) {
307
+ // Keep the turn and the quote across a refusal that a person can fix,
308
+ // so the rerun (with --approved, or a stated max_rows) reuses them.
309
+ if (!input.turnToken && turnToken && (code(error) === 'quote_required' || code(error) === 'approval_required')) {
310
+ if (code(error) === 'approval_required') {
311
+ 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');
312
+ }
313
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quoteHash ? { quote_hash: quoteHash, fingerprint } : undefined);
314
+ }
315
+ // Only an unknown or transient result may resume an attempt. A server
316
+ // refusal with retryable:false is terminal and must receive a new key.
317
+ const transient = error instanceof client_1.CliError && (error.code === 'request_timeout' || error.code === 'network_error' || error.retryable === true || error.status >= 500);
318
+ if (attemptIdentity && !transient)
319
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
320
+ throw error;
321
+ }
322
+ if (!input.turnToken) {
323
+ if (body.mode === 'dry_run' && turnToken) {
324
+ const quote = quoteFromResult(result);
325
+ await (0, turnCache_1.rememberTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, turnToken, Date.now(), warnTurnCache, quote ? { quote_hash: quote.quote_hash, fingerprint } : undefined);
326
+ }
327
+ else if (pending || ranDryRun)
328
+ await (0, turnCache_1.forgetTurnToken)(context.turnsPath, context.baseUrl, context.turnKey, capabilityId, warnTurnCache);
329
+ }
330
+ let timedOut = false;
331
+ if (!input.noWait && !posture?.returnsRunId) {
332
+ const waited = await (0, runWait_1.waitForRun)(client, result, input.waitTimeout, context.sleep);
333
+ result = waited.value;
334
+ timedOut = waited.timedOut;
335
+ }
336
+ // A run still active after waiting has unknown outcome; retain its key for
337
+ // the read-back/retry rule. A terminal response completes this attempt.
338
+ if (attemptIdentity && !timedOut)
339
+ await (0, idempotency_1.finishIdempotencyAttempt)(idempotencyPath, attemptIdentity);
340
+ return { value: withIdempotencyMeta(result, explicitKey ? null : sentKey), timedOut, idempotencyKey: sentKey, turnToken: turnToken ?? null };
341
+ }
342
+ /** A read the CLI needs before it can form a write, run through the same gate as any other run. */
343
+ async function readCapability(context, capabilityId, params, waitTimeout) {
344
+ const outcome = await runGated(context, { capabilityId, body: { params }, waitTimeout });
345
+ return outcome.value;
346
+ }