@compaction/cli 0.1.4 → 0.3.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 (170) hide show
  1. package/README.md +51 -12
  2. package/dist/cli/commands/activity.d.ts +8 -0
  3. package/dist/cli/commands/activity.js +35 -0
  4. package/dist/cli/commands/apply-context.js +68 -0
  5. package/dist/cli/commands/billing-delta.js +12 -1
  6. package/dist/cli/commands/capture-claude-code.d.ts +44 -0
  7. package/dist/cli/commands/capture-claude-code.js +206 -0
  8. package/dist/cli/commands/capture.js +283 -1
  9. package/dist/cli/commands/compact.js +24 -11
  10. package/dist/cli/commands/context.d.ts +2 -0
  11. package/dist/cli/commands/context.js +130 -0
  12. package/dist/cli/commands/dev.d.ts +17 -0
  13. package/dist/cli/commands/dev.js +131 -0
  14. package/dist/cli/commands/gateway.d.ts +31 -0
  15. package/dist/cli/commands/gateway.js +312 -0
  16. package/dist/cli/commands/hooks.d.ts +5 -0
  17. package/dist/cli/commands/hooks.js +181 -0
  18. package/dist/cli/commands/import.js +3 -1
  19. package/dist/cli/commands/init.js +422 -17
  20. package/dist/cli/commands/input-compaction-ab.d.ts +2 -0
  21. package/dist/cli/commands/input-compaction-ab.js +125 -0
  22. package/dist/cli/commands/optimize-hosted.d.ts +54 -0
  23. package/dist/cli/commands/optimize-hosted.js +123 -0
  24. package/dist/cli/commands/optimize.js +8 -0
  25. package/dist/cli/commands/output-shaping-ab.d.ts +2 -0
  26. package/dist/cli/commands/output-shaping-ab.js +132 -0
  27. package/dist/cli/commands/output-shaping.d.ts +7 -0
  28. package/dist/cli/commands/output-shaping.js +43 -0
  29. package/dist/cli/commands/policies.d.ts +2 -0
  30. package/dist/cli/commands/policies.js +80 -0
  31. package/dist/cli/commands/precall.d.ts +19 -0
  32. package/dist/cli/commands/precall.js +436 -0
  33. package/dist/cli/commands/recommend.js +1 -1
  34. package/dist/cli/commands/run.js +310 -18
  35. package/dist/cli/commands/upgrade-status.d.ts +42 -0
  36. package/dist/cli/commands/upgrade-status.js +152 -0
  37. package/dist/cli/cursor-export-read.d.ts +13 -0
  38. package/dist/cli/cursor-export-read.js +53 -0
  39. package/dist/cli/cursor-live-preflight.d.ts +19 -0
  40. package/dist/cli/cursor-live-preflight.js +46 -0
  41. package/dist/cli/index.js +39 -2
  42. package/dist/cli/onboarding/GatewayTui.d.ts +33 -0
  43. package/dist/cli/onboarding/GatewayTui.js +113 -0
  44. package/dist/cli/onboarding/model.d.ts +32 -1
  45. package/dist/cli/onboarding/model.js +27 -2
  46. package/dist/cli/terminal-logo.d.ts +27 -0
  47. package/dist/cli/terminal-logo.js +49 -0
  48. package/dist/core/activity-event.d.ts +107 -0
  49. package/dist/core/activity-event.js +182 -0
  50. package/dist/core/activity-store.d.ts +63 -0
  51. package/dist/core/activity-store.js +254 -0
  52. package/dist/core/activity-view.d.ts +76 -0
  53. package/dist/core/activity-view.js +120 -0
  54. package/dist/core/api-client/index.d.ts +3 -1
  55. package/dist/core/api-client/index.js +4 -0
  56. package/dist/core/api-client/payload.d.ts +5 -1
  57. package/dist/core/api-client/payload.js +8 -6
  58. package/dist/core/api-client/persisted-config.d.ts +57 -0
  59. package/dist/core/api-client/persisted-config.js +156 -0
  60. package/dist/core/api-client/tool.d.ts +29 -0
  61. package/dist/core/api-client/tool.js +50 -0
  62. package/dist/core/api-client/types.d.ts +15 -4
  63. package/dist/core/auto-apply-ask.d.ts +58 -0
  64. package/dist/core/auto-apply-ask.js +105 -0
  65. package/dist/core/auto-apply-gates.d.ts +76 -0
  66. package/dist/core/auto-apply-gates.js +113 -0
  67. package/dist/core/before-call-activity.d.ts +35 -0
  68. package/dist/core/before-call-activity.js +103 -0
  69. package/dist/core/before-call-recovery.d.ts +19 -0
  70. package/dist/core/before-call-recovery.js +46 -0
  71. package/dist/core/before-call-stdin.d.ts +59 -0
  72. package/dist/core/before-call-stdin.js +78 -0
  73. package/dist/core/before-call.d.ts +149 -0
  74. package/dist/core/before-call.js +358 -0
  75. package/dist/core/billing-delta/billing-delta-record.d.ts +28 -0
  76. package/dist/core/billing-delta/billing-delta-record.js +26 -0
  77. package/dist/core/capture-record.d.ts +40 -0
  78. package/dist/core/capture-record.js +55 -0
  79. package/dist/core/claude-code-before-call.d.ts +37 -0
  80. package/dist/core/claude-code-before-call.js +120 -0
  81. package/dist/core/claude-code-connect.d.ts +31 -0
  82. package/dist/core/claude-code-connect.js +87 -0
  83. package/dist/core/claude-code-hook-record.d.ts +71 -0
  84. package/dist/core/claude-code-hook-record.js +0 -0
  85. package/dist/core/claude-code-hooks.d.ts +77 -0
  86. package/dist/core/claude-code-hooks.js +141 -0
  87. package/dist/core/codex-capture.d.ts +45 -0
  88. package/dist/core/codex-capture.js +204 -0
  89. package/dist/core/command-runner.js +5 -1
  90. package/dist/core/compaction-artifacts.d.ts +51 -0
  91. package/dist/core/compaction-artifacts.js +242 -0
  92. package/dist/core/compactor.d.ts +9 -0
  93. package/dist/core/compactor.js +95 -0
  94. package/dist/core/context-store-eval-cases.d.ts +6 -0
  95. package/dist/core/context-store-eval-cases.js +331 -0
  96. package/dist/core/context-store-eval.d.ts +140 -0
  97. package/dist/core/context-store-eval.js +138 -0
  98. package/dist/core/context-store-fs.d.ts +73 -0
  99. package/dist/core/context-store-fs.js +157 -0
  100. package/dist/core/context-store-sufficiency.d.ts +98 -0
  101. package/dist/core/context-store-sufficiency.js +135 -0
  102. package/dist/core/context-store.d.ts +155 -0
  103. package/dist/core/context-store.js +228 -0
  104. package/dist/core/cross-surface-event.d.ts +306 -0
  105. package/dist/core/cross-surface-event.js +330 -0
  106. package/dist/core/cursor-capture.d.ts +54 -0
  107. package/dist/core/cursor-capture.js +215 -0
  108. package/dist/core/cursor-preflight-probe.d.ts +8 -0
  109. package/dist/core/cursor-preflight-probe.js +88 -0
  110. package/dist/core/cursor-preflight.d.ts +86 -0
  111. package/dist/core/cursor-preflight.js +126 -0
  112. package/dist/core/gateway/apply-activation.d.ts +39 -0
  113. package/dist/core/gateway/apply-activation.js +84 -0
  114. package/dist/core/gateway/apply-policy.d.ts +64 -0
  115. package/dist/core/gateway/apply-policy.js +221 -0
  116. package/dist/core/gateway/apply-receipt.d.ts +36 -0
  117. package/dist/core/gateway/apply-receipt.js +75 -0
  118. package/dist/core/gateway/cache-proof.d.ts +45 -0
  119. package/dist/core/gateway/cache-proof.js +65 -0
  120. package/dist/core/gateway/configure.d.ts +50 -0
  121. package/dist/core/gateway/configure.js +169 -0
  122. package/dist/core/gateway/openai-usage.d.ts +56 -0
  123. package/dist/core/gateway/openai-usage.js +128 -0
  124. package/dist/core/gateway/receipt.d.ts +138 -0
  125. package/dist/core/gateway/receipt.js +120 -0
  126. package/dist/core/gateway/recovery.d.ts +23 -0
  127. package/dist/core/gateway/recovery.js +68 -0
  128. package/dist/core/gateway/server.d.ts +51 -0
  129. package/dist/core/gateway/server.js +276 -0
  130. package/dist/core/gateway/status.d.ts +45 -0
  131. package/dist/core/gateway/status.js +109 -0
  132. package/dist/core/hook-usage-aggregate.d.ts +47 -0
  133. package/dist/core/hook-usage-aggregate.js +161 -0
  134. package/dist/core/input-compaction-ab.d.ts +111 -0
  135. package/dist/core/input-compaction-ab.js +158 -0
  136. package/dist/core/local-run-record.d.ts +109 -0
  137. package/dist/core/local-run-record.js +223 -0
  138. package/dist/core/output-shaping-ab.d.ts +140 -0
  139. package/dist/core/output-shaping-ab.js +146 -0
  140. package/dist/core/output-shaping-attach.d.ts +31 -0
  141. package/dist/core/output-shaping-attach.js +57 -0
  142. package/dist/core/output-shaping.d.ts +56 -0
  143. package/dist/core/output-shaping.js +89 -0
  144. package/dist/core/policy-middleware.d.ts +121 -0
  145. package/dist/core/policy-middleware.js +919 -0
  146. package/dist/core/policy-preferences.d.ts +99 -0
  147. package/dist/core/policy-preferences.js +232 -0
  148. package/dist/core/report-generator.d.ts +19 -1
  149. package/dist/core/report-generator.js +51 -0
  150. package/dist/core/run-aggregator.d.ts +56 -1
  151. package/dist/core/run-aggregator.js +93 -2
  152. package/dist/core/run-flow-report.d.ts +82 -0
  153. package/dist/core/run-flow-report.js +71 -0
  154. package/dist/core/safety-report.js +8 -1
  155. package/dist/core/shim-capture-bridge.d.ts +32 -0
  156. package/dist/core/shim-capture-bridge.js +88 -0
  157. package/dist/core/skill-injection-policy.d.ts +72 -0
  158. package/dist/core/skill-injection-policy.js +183 -0
  159. package/dist/core/spend-attribution.js +1 -1
  160. package/dist/core/token-accounting.d.ts +1 -1
  161. package/dist/core/tool-shim.d.ts +129 -0
  162. package/dist/core/tool-shim.js +447 -0
  163. package/dist/core/trace-adapters.d.ts +8 -0
  164. package/dist/core/trace-adapters.js +4 -0
  165. package/dist/core/trace-parser.d.ts +13 -13
  166. package/dist/core/trace-parser.js +6 -6
  167. package/dist/core/types.d.ts +45 -2
  168. package/dist/core/waste-detector.d.ts +20 -0
  169. package/dist/core/waste-detector.js +160 -6
  170. package/package.json +1 -1
@@ -0,0 +1,120 @@
1
+ export const DEFAULT_ACTIVITY_LIMIT = 20;
2
+ export const NO_ACTIVITY_MESSAGE = "No activity recorded yet — run a supported workflow (e.g. `compaction run <tool> -- …`) and it will appear here.";
3
+ function axisSource(event, axis) {
4
+ const axisData = event.token_source?.[axis];
5
+ if (!axisData)
6
+ return { source: "unknown" };
7
+ return { source: axisData.source, reason: axisData.unavailable_reason };
8
+ }
9
+ function autoApplyStatus(event) {
10
+ const auto = event.auto_apply;
11
+ if (!auto)
12
+ return "n/a";
13
+ if (auto.applied_automatically === true)
14
+ return "auto-applied (policy)";
15
+ if (auto.preference === "auto-when-gates-pass")
16
+ return "preference: auto-when-gates-pass (application pending)";
17
+ return "ask each time";
18
+ }
19
+ /** Build the rendered rows: newest first, optional surface filter, then limit. */
20
+ export function buildActivityRows(events, options = {}) {
21
+ const limit = options.limit ?? DEFAULT_ACTIVITY_LIMIT;
22
+ // The store appends chronologically; most recent is the LAST line. Newest-first for display.
23
+ const ordered = [...events].reverse();
24
+ const filtered = options.surface ? ordered.filter((event) => event.surface === options.surface) : ordered;
25
+ const shown = filtered.slice(0, Math.max(0, limit));
26
+ return shown.map((event, index) => {
27
+ const input = axisSource(event, "input");
28
+ const output = axisSource(event, "output");
29
+ const outputUnavailable = output.source === "unavailable" || output.source === "unknown";
30
+ const outputCount = typeof event.output_after === "number"
31
+ ? event.output_after
32
+ : typeof event.output_before === "number"
33
+ ? event.output_before
34
+ : typeof event.output_estimate === "number"
35
+ ? event.output_estimate
36
+ : null;
37
+ return {
38
+ position: index + 1,
39
+ activity_event_id: event.activity_event_id ?? "unknown",
40
+ surface: event.surface,
41
+ provider: event.provider ?? "unknown",
42
+ model_label: event.model_label ?? "unknown",
43
+ input_before: typeof event.input_before === "number" ? event.input_before : null,
44
+ input_after: typeof event.input_after === "number" ? event.input_after : null,
45
+ input_source: input.source,
46
+ ...(input.reason ? { input_unavailable_reason: input.reason } : {}),
47
+ output_tokens: outputUnavailable ? null : outputCount,
48
+ output_source: output.source,
49
+ ...(output.reason ? { output_unavailable_reason: output.reason } : {}),
50
+ policy_used: event.policy_used ?? "none",
51
+ approval_status: event.approval_status ?? "not-recorded",
52
+ auto_apply_status: autoApplyStatus(event),
53
+ sync_status: event.sync_status ?? "local-only"
54
+ };
55
+ });
56
+ }
57
+ function renderInputCell(row) {
58
+ if (row.input_source === "unavailable")
59
+ return `unavailable (${row.input_source})`;
60
+ if (row.input_before === null)
61
+ return `— (${row.input_source})`;
62
+ // Only show before→after when a real reshaping happened (both present); else a measured count.
63
+ if (row.input_after === null)
64
+ return `${row.input_before} measured (${row.input_source})`;
65
+ return `${row.input_before}→${row.input_after} (${row.input_source})`;
66
+ }
67
+ function renderOutputCell(row) {
68
+ if (row.output_tokens === null)
69
+ return `unavailable (${row.output_source})`;
70
+ return `${row.output_tokens} measured (${row.output_source})`;
71
+ }
72
+ export function buildActivityJson(rows, meta) {
73
+ return {
74
+ activity: rows,
75
+ meta: {
76
+ total_events: meta.totalEvents,
77
+ shown: rows.length,
78
+ limit: meta.limit,
79
+ surface_filter: meta.surface ?? null,
80
+ ordering: "append-recency (events carry no wall-clock time); position 1 = most recent",
81
+ labels: "token counts are shown at each event's own axis tier (local-estimate | provider-reported | unavailable); output is a measured token count, never a savings figure; no cost or billing figure is shown"
82
+ },
83
+ ...(meta.skippedCount && meta.skippedCount > 0
84
+ ? { skipped_note: `${meta.skippedCount} malformed line(s) were skipped (not shown)` }
85
+ : {})
86
+ };
87
+ }
88
+ /** Human table. Content-free; honest labels; a header note that ordering is append-recency. */
89
+ export function formatActivityTable(rows, meta) {
90
+ if (rows.length === 0) {
91
+ if (meta.surface) {
92
+ return `No activity recorded for surface "${meta.surface}" yet. ${NO_ACTIVITY_MESSAGE}`;
93
+ }
94
+ return NO_ACTIVITY_MESSAGE;
95
+ }
96
+ const header = ["#", "surface", "provider/model", "input (before→after)", "output tokens", "policy", "approval", "auto-apply", "sync"];
97
+ const table = rows.map((row) => [
98
+ `${row.position}`,
99
+ row.surface,
100
+ `${row.provider}/${row.model_label}`,
101
+ renderInputCell(row),
102
+ renderOutputCell(row),
103
+ row.policy_used,
104
+ row.approval_status,
105
+ row.auto_apply_status,
106
+ row.sync_status
107
+ ]);
108
+ const widths = header.map((cell, col) => Math.max(cell.length, ...table.map((line) => line[col].length)));
109
+ const pad = (cells) => cells.map((cell, col) => cell.padEnd(widths[col])).join(" ");
110
+ const surfaceNote = meta.surface ? ` (surface "${meta.surface}")` : "";
111
+ const preamble = [
112
+ `Recent activity${surfaceNote}: showing ${rows.length} of ${meta.totalEvents} recorded run(s).`,
113
+ `Ordered by append recency (activity events carry no wall-clock time); #1 = most recent.`,
114
+ `Token counts are local estimates unless labeled provider-reported; output is a measured count, not a savings figure.`
115
+ ];
116
+ const lines = [pad(header), pad(header.map((_, col) => "-".repeat(widths[col]))), ...table.map(pad)];
117
+ const trailer = meta.skippedCount && meta.skippedCount > 0 ? [``, `(${meta.skippedCount} malformed line(s) skipped.)`] : [];
118
+ return [...preamble, ``, ...lines, ...trailer].join("\n");
119
+ }
120
+ //# sourceMappingURL=activity-view.js.map
@@ -8,6 +8,8 @@
8
8
  * explicitly invokes `apiStatus` / `sendRequest`.
9
9
  */
10
10
  export { DEFAULT_API_URL, DEFAULT_TIMEOUT_MS, resolveApiConfig, buildHeaders, type ApiConfig, type EnvLike } from "./config.js";
11
+ export { configDir, configPath, readPersistedConfig, writePersistedConfig, maskKey, isLocalDefaultUrl, urlHost, resolveTarget, healthCheckConfig, type PersistedConfig, type ResolvedTarget, type Source } from "./persisted-config.js";
11
12
  export { isContentBearing, ConsentError, buildOptimizeRequest, buildEvaluateRequest, buildReportsRequest, previewPayload, type OptimizeBuildOptions, type EvaluateBuildOptions, type ReportsBuildOptions, type PayloadPreview } from "./payload.js";
12
13
  export { apiStatus, sendRequest, ApiNotConfirmedError, ApiTransportError, type ApiResponse, type SendOptions } from "./client.js";
13
- export { CONTENT_BEARING_PAYLOAD_CLASSES, type PayloadClass, type TokenSource, type Consent, type Trace, type TokenUsage, type OptimizeRequest, type EvaluateRequest, type ReportsRequest, type ApiRequestBody, type StatusResponse, type ApiErrorBody } from "./types.js";
14
+ export { CONTENT_BEARING_PAYLOAD_CLASSES, type PayloadClass, type TokenSource, type Consent, type Trace, type TokenUsage, type ProviderMetadata, type OptimizeRequest, type EvaluateRequest, type ReportsRequest, type ApiRequestBody, type StatusResponse, type ApiErrorBody } from "./types.js";
15
+ export { TOOL_NAMES, isToolName, resolveToolName, type ToolName } from "./tool.js";
@@ -8,7 +8,11 @@
8
8
  * explicitly invokes `apiStatus` / `sendRequest`.
9
9
  */
10
10
  export { DEFAULT_API_URL, DEFAULT_TIMEOUT_MS, resolveApiConfig, buildHeaders } from "./config.js";
11
+ // ADDITIVE opt-in persisted-config layer (upgrade/status). Does NOT change the env-only
12
+ // `resolveApiConfig` contract above.
13
+ export { configDir, configPath, readPersistedConfig, writePersistedConfig, maskKey, isLocalDefaultUrl, urlHost, resolveTarget, healthCheckConfig } from "./persisted-config.js";
11
14
  export { isContentBearing, ConsentError, buildOptimizeRequest, buildEvaluateRequest, buildReportsRequest, previewPayload } from "./payload.js";
12
15
  export { apiStatus, sendRequest, ApiNotConfirmedError, ApiTransportError } from "./client.js";
13
16
  export { CONTENT_BEARING_PAYLOAD_CLASSES } from "./types.js";
17
+ export { TOOL_NAMES, isToolName, resolveToolName } from "./tool.js";
14
18
  //# sourceMappingURL=index.js.map
@@ -7,7 +7,7 @@
7
7
  * `enforceConsent`. The preview shows EXACTLY what would be sent (API key redacted), so the
8
8
  * user never sends blind. See `docs/ops/cli-api-client-design.md` §3–§5.
9
9
  */
10
- import { type EvaluateRequest, type OptimizeRequest, type PayloadClass, type ReportsRequest, type Trace } from "./types.js";
10
+ import { type EvaluateRequest, type OptimizeRequest, type PayloadClass, type ReportsRequest, type ToolName, type Trace } from "./types.js";
11
11
  /** True when a class carries raw-or-sanitized message CONTENT (requires explicit consent). */
12
12
  export declare function isContentBearing(payloadClass: PayloadClass | undefined): boolean;
13
13
  /** Thrown by a builder when consent rules are violated. The client must fail closed. */
@@ -36,6 +36,10 @@ export interface OptimizeBuildOptions {
36
36
  outputTokens?: number;
37
37
  provider?: string;
38
38
  runtime?: string;
39
+ /** Content-free coding-tool identity (which tool produced the captured session). */
40
+ tool?: ToolName;
41
+ /** Content-free model identifier (e.g. "claude-sonnet-4-6"); never request content. */
42
+ model?: string;
39
43
  labels?: {
40
44
  project?: string;
41
45
  session?: string;
@@ -94,12 +94,14 @@ export function buildOptimizeRequest(opts) {
94
94
  },
95
95
  optimization_mode: opts.optimizationMode ?? "recommend"
96
96
  };
97
- if (opts.provider || opts.runtime) {
98
- req.provider_metadata = {
99
- ...(opts.provider ? { provider: opts.provider } : {}),
100
- ...(opts.runtime ? { runtime: opts.runtime } : {})
101
- };
102
- }
97
+ const providerMetadata = {
98
+ ...(opts.provider ? { provider: opts.provider } : {}),
99
+ ...(opts.runtime ? { runtime: opts.runtime } : {}),
100
+ ...(opts.tool ? { tool: opts.tool } : {}),
101
+ ...(opts.model ? { model: opts.model } : {})
102
+ };
103
+ if (Object.keys(providerMetadata).length > 0)
104
+ req.provider_metadata = providerMetadata;
103
105
  if (opts.labels)
104
106
  req.labels = opts.labels;
105
107
  return req;
@@ -0,0 +1,57 @@
1
+ import { type ApiConfig, type EnvLike } from "./config.js";
2
+ /** On-disk shape. Only these two fields are ever written; nothing else is persisted. */
3
+ export interface PersistedConfig {
4
+ api_url: string;
5
+ api_key: string;
6
+ }
7
+ /** Where a value came from in the resolution precedence (for honest status reporting). */
8
+ export type Source = "flag" | "env" | "file" | "default" | "none";
9
+ export interface ResolvedTarget {
10
+ /** Resolved base URL (no trailing slash). */
11
+ url: string;
12
+ /** Resolved bearer token, or `undefined` when none is configured anywhere. */
13
+ apiKey?: string;
14
+ urlSource: Source;
15
+ keySource: Source;
16
+ }
17
+ /** Resolve the config directory. `COMPACTION_CONFIG_DIR` overrides `~/.compaction` (tests use this). */
18
+ export declare function configDir(env?: EnvLike): string;
19
+ /** Absolute path to the persisted config file. */
20
+ export declare function configPath(env?: EnvLike): string;
21
+ /**
22
+ * Read the persisted config, or `undefined` if absent/empty/malformed. Never throws: a corrupt or
23
+ * partial file resolves to `undefined` (fail-closed to "not configured") rather than crashing a
24
+ * command. Only returns a value when BOTH `api_url` and `api_key` are non-empty strings.
25
+ */
26
+ export declare function readPersistedConfig(env?: EnvLike): PersistedConfig | undefined;
27
+ /**
28
+ * Persist `{api_url, api_key}` at `~/.compaction/config.json` with mode 0600 (dir 0700). The key
29
+ * touches disk ONLY here. Returns the absolute path written. `chmod` is applied explicitly AFTER the
30
+ * write because `writeFileSync`'s `mode` only takes effect on file CREATION (a pre-existing file
31
+ * keeps its old mode otherwise).
32
+ */
33
+ export declare function writePersistedConfig(config: PersistedConfig, env?: EnvLike): string;
34
+ /**
35
+ * Mask an API key for display. Shows a short prefix + the last 4 characters
36
+ * (e.g. `ck_live_…a1b2`). A key too short to mask safely (< 16 chars, where a prefix+suffix would
37
+ * reveal most of it) is FULLY masked. Empty → `(none)`. The full key is NEVER returned.
38
+ */
39
+ export declare function maskKey(key: string | undefined): string;
40
+ /** True iff the resolved URL is the LOCAL-dev default — i.e. NO private/hosted endpoint is set. */
41
+ export declare function isLocalDefaultUrl(url: string): boolean;
42
+ /** Extract the HOST (host:port, no scheme/path) for display. Falls back to the raw string. */
43
+ export declare function urlHost(url: string): string;
44
+ /**
45
+ * Resolve the effective target for the upgrade/status commands.
46
+ *
47
+ * Precedence (independently for url and key): explicit flag > env
48
+ * (`COMPACTION_API_URL` / `COMPACTION_API_KEY`) > persisted file > `DEFAULT_API_URL` (url) / none
49
+ * (key). Pure: it reads the given env + the persisted file and returns a plain object.
50
+ */
51
+ export declare function resolveTarget(opts: {
52
+ flagUrl?: string;
53
+ flagKey?: string;
54
+ env?: EnvLike;
55
+ }): ResolvedTarget;
56
+ /** Build a bounded-timeout `ApiConfig` for a health check from a resolved target. */
57
+ export declare function healthCheckConfig(target: ResolvedTarget, timeoutMs?: number): ApiConfig;
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Compaction API client — PERSISTED configuration (PUBLIC CLI/SDK code).
3
+ *
4
+ * ADDITIVE layer over the env-only `config.ts`: `config.ts` stays pure/env-only and its
5
+ * `resolveApiConfig` contract is UNCHANGED. This module adds an OPT-IN on-disk store used ONLY by
6
+ * `compaction upgrade` / `compaction status` so a private-beta / self-hosted user can save an
7
+ * endpoint + key once instead of re-exporting env vars every shell.
8
+ *
9
+ * HARD RAILS:
10
+ * - The file lives at `~/.compaction/config.json`, written with mode 0600 (dir 0700). An env
11
+ * override `COMPACTION_CONFIG_DIR` redirects it (tests point it at a tmpdir; the real
12
+ * `~/.compaction` is NEVER touched by tests).
13
+ * - There is NO baked-in remote URL — the default stays the LOCAL-dev `DEFAULT_API_URL`.
14
+ * - The API key is written ONLY to this 0600 file. `maskKey` is the ONLY form that may ever be
15
+ * printed, logged, or placed in an artifact/error; the full key is never surfaced anywhere else.
16
+ */
17
+ import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
+ import { homedir } from "node:os";
19
+ import { join } from "node:path";
20
+ import { DEFAULT_API_URL, DEFAULT_TIMEOUT_MS } from "./config.js";
21
+ function stripTrailingSlash(url) {
22
+ return url.endsWith("/") ? url.slice(0, -1) : url;
23
+ }
24
+ /** Resolve the config directory. `COMPACTION_CONFIG_DIR` overrides `~/.compaction` (tests use this). */
25
+ export function configDir(env = process.env) {
26
+ const override = (env.COMPACTION_CONFIG_DIR ?? "").trim();
27
+ return override !== "" ? override : join(homedir(), ".compaction");
28
+ }
29
+ /** Absolute path to the persisted config file. */
30
+ export function configPath(env = process.env) {
31
+ return join(configDir(env), "config.json");
32
+ }
33
+ /**
34
+ * Read the persisted config, or `undefined` if absent/empty/malformed. Never throws: a corrupt or
35
+ * partial file resolves to `undefined` (fail-closed to "not configured") rather than crashing a
36
+ * command. Only returns a value when BOTH `api_url` and `api_key` are non-empty strings.
37
+ */
38
+ export function readPersistedConfig(env = process.env) {
39
+ const path = configPath(env);
40
+ if (!existsSync(path))
41
+ return undefined;
42
+ try {
43
+ const raw = JSON.parse(readFileSync(path, "utf8"));
44
+ const url = typeof raw.api_url === "string" ? raw.api_url.trim() : "";
45
+ const key = typeof raw.api_key === "string" ? raw.api_key.trim() : "";
46
+ if (url === "" || key === "")
47
+ return undefined;
48
+ return { api_url: stripTrailingSlash(url), api_key: key };
49
+ }
50
+ catch {
51
+ return undefined;
52
+ }
53
+ }
54
+ /**
55
+ * Persist `{api_url, api_key}` at `~/.compaction/config.json` with mode 0600 (dir 0700). The key
56
+ * touches disk ONLY here. Returns the absolute path written. `chmod` is applied explicitly AFTER the
57
+ * write because `writeFileSync`'s `mode` only takes effect on file CREATION (a pre-existing file
58
+ * keeps its old mode otherwise).
59
+ */
60
+ export function writePersistedConfig(config, env = process.env) {
61
+ const dir = configDir(env);
62
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
63
+ const path = join(dir, "config.json");
64
+ const body = `${JSON.stringify({ api_url: stripTrailingSlash(config.api_url.trim()), api_key: config.api_key.trim() }, null, 2)}\n`;
65
+ writeFileSync(path, body, { mode: 0o600 });
66
+ chmodSync(path, 0o600);
67
+ return path;
68
+ }
69
+ /**
70
+ * Mask an API key for display. Shows a short prefix + the last 4 characters
71
+ * (e.g. `ck_live_…a1b2`). A key too short to mask safely (< 16 chars, where a prefix+suffix would
72
+ * reveal most of it) is FULLY masked. Empty → `(none)`. The full key is NEVER returned.
73
+ */
74
+ export function maskKey(key) {
75
+ const k = (key ?? "").trim();
76
+ if (k === "")
77
+ return "(none)";
78
+ // Too short to reveal a prefix AND a suffix without exposing most of the key → fully mask.
79
+ if (k.length < 16)
80
+ return "…(hidden)";
81
+ return `${k.slice(0, 8)}…${k.slice(-4)}`;
82
+ }
83
+ /** True iff the resolved URL is the LOCAL-dev default — i.e. NO private/hosted endpoint is set. */
84
+ export function isLocalDefaultUrl(url) {
85
+ return stripTrailingSlash(url.trim()) === DEFAULT_API_URL;
86
+ }
87
+ /** Extract the HOST (host:port, no scheme/path) for display. Falls back to the raw string. */
88
+ export function urlHost(url) {
89
+ try {
90
+ return new URL(url).host;
91
+ }
92
+ catch {
93
+ return url;
94
+ }
95
+ }
96
+ /**
97
+ * Resolve the effective target for the upgrade/status commands.
98
+ *
99
+ * Precedence (independently for url and key): explicit flag > env
100
+ * (`COMPACTION_API_URL` / `COMPACTION_API_KEY`) > persisted file > `DEFAULT_API_URL` (url) / none
101
+ * (key). Pure: it reads the given env + the persisted file and returns a plain object.
102
+ */
103
+ export function resolveTarget(opts) {
104
+ const env = opts.env ?? process.env;
105
+ const file = readPersistedConfig(env);
106
+ const flagUrl = (opts.flagUrl ?? "").trim();
107
+ const envUrl = (env.COMPACTION_API_URL ?? "").trim();
108
+ let url;
109
+ let urlSource;
110
+ if (flagUrl !== "") {
111
+ url = flagUrl;
112
+ urlSource = "flag";
113
+ }
114
+ else if (envUrl !== "") {
115
+ url = envUrl;
116
+ urlSource = "env";
117
+ }
118
+ else if (file) {
119
+ url = file.api_url;
120
+ urlSource = "file";
121
+ }
122
+ else {
123
+ url = DEFAULT_API_URL;
124
+ urlSource = "default";
125
+ }
126
+ const flagKey = (opts.flagKey ?? "").trim();
127
+ const envKey = (env.COMPACTION_API_KEY ?? "").trim();
128
+ let apiKey;
129
+ let keySource;
130
+ if (flagKey !== "") {
131
+ apiKey = flagKey;
132
+ keySource = "flag";
133
+ }
134
+ else if (envKey !== "") {
135
+ apiKey = envKey;
136
+ keySource = "env";
137
+ }
138
+ else if (file) {
139
+ apiKey = file.api_key;
140
+ keySource = "file";
141
+ }
142
+ else {
143
+ apiKey = undefined;
144
+ keySource = "none";
145
+ }
146
+ return { url: stripTrailingSlash(url), apiKey, urlSource, keySource };
147
+ }
148
+ /** Build a bounded-timeout `ApiConfig` for a health check from a resolved target. */
149
+ export function healthCheckConfig(target, timeoutMs = 5000) {
150
+ return {
151
+ url: target.url,
152
+ apiKey: target.apiKey,
153
+ timeoutMs: timeoutMs > 0 ? timeoutMs : DEFAULT_TIMEOUT_MS
154
+ };
155
+ }
156
+ //# sourceMappingURL=persisted-config.js.map
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Canonical coding-tool identity (PUBLIC CLI/SDK code).
3
+ *
4
+ * The single source of truth for the `tool` attribution value the CLI tags onto a recorded run.
5
+ * It is CONTENT-FREE metadata — a short, fixed identifier of WHICH coding tool produced a captured
6
+ * session (e.g. Claude Code vs Codex vs Cursor), never any prompt/completion/trace content.
7
+ *
8
+ * This union is hand-mirrored — with no cross-package import — by the `tool` enum in
9
+ * `apps/api/src/schemas.ts` and the `tool` CHECK constraint in `apps/control-plane` migration
10
+ * 0007 / 0008. Keep the five values in lock-step across all three.
11
+ *
12
+ * `cursor` is a first-class recognized value so a Cursor run records correctly the moment a Cursor
13
+ * capture adapter lands. No live Cursor (or Codex) CAPTURE adapter ships today — Codex is import
14
+ * only and Cursor's session format is not yet verified — so a real CLI capture currently resolves
15
+ * to `claude-code` / `openai-agents`, or `other` when the source is unknown. The wire never carries
16
+ * a tool the producing adapter did not actually set.
17
+ */
18
+ export type ToolName = "claude-code" | "codex" | "cursor" | "openai-agents" | "other";
19
+ /** The five canonical tool values, in declaration order. */
20
+ export declare const TOOL_NAMES: readonly ToolName[];
21
+ /** Narrowing guard: true iff `value` is one of the canonical tool names. */
22
+ export declare function isToolName(value: unknown): value is ToolName;
23
+ /**
24
+ * Resolve a CONTENT-FREE tool identifier from a capture-adapter id or a trace-source string
25
+ * (e.g. `provenance.captureAdapter` or `trace.source`). Honest by construction: an unknown or
26
+ * absent hint resolves to `other` rather than guessing a specific tool. Never returns a value the
27
+ * caller did not actually produce.
28
+ */
29
+ export declare function resolveToolName(hint: string | null | undefined): ToolName;
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Canonical coding-tool identity (PUBLIC CLI/SDK code).
3
+ *
4
+ * The single source of truth for the `tool` attribution value the CLI tags onto a recorded run.
5
+ * It is CONTENT-FREE metadata — a short, fixed identifier of WHICH coding tool produced a captured
6
+ * session (e.g. Claude Code vs Codex vs Cursor), never any prompt/completion/trace content.
7
+ *
8
+ * This union is hand-mirrored — with no cross-package import — by the `tool` enum in
9
+ * `apps/api/src/schemas.ts` and the `tool` CHECK constraint in `apps/control-plane` migration
10
+ * 0007 / 0008. Keep the five values in lock-step across all three.
11
+ *
12
+ * `cursor` is a first-class recognized value so a Cursor run records correctly the moment a Cursor
13
+ * capture adapter lands. No live Cursor (or Codex) CAPTURE adapter ships today — Codex is import
14
+ * only and Cursor's session format is not yet verified — so a real CLI capture currently resolves
15
+ * to `claude-code` / `openai-agents`, or `other` when the source is unknown. The wire never carries
16
+ * a tool the producing adapter did not actually set.
17
+ */
18
+ /** The five canonical tool values, in declaration order. */
19
+ export const TOOL_NAMES = [
20
+ "claude-code",
21
+ "codex",
22
+ "cursor",
23
+ "openai-agents",
24
+ "other"
25
+ ];
26
+ /** Narrowing guard: true iff `value` is one of the canonical tool names. */
27
+ export function isToolName(value) {
28
+ return typeof value === "string" && TOOL_NAMES.includes(value);
29
+ }
30
+ /**
31
+ * Resolve a CONTENT-FREE tool identifier from a capture-adapter id or a trace-source string
32
+ * (e.g. `provenance.captureAdapter` or `trace.source`). Honest by construction: an unknown or
33
+ * absent hint resolves to `other` rather than guessing a specific tool. Never returns a value the
34
+ * caller did not actually produce.
35
+ */
36
+ export function resolveToolName(hint) {
37
+ if (!hint)
38
+ return "other";
39
+ const h = hint.trim().toLowerCase();
40
+ if (h === "claude-code" || h === "claude_code")
41
+ return "claude-code";
42
+ if (h === "openai-agents" || h === "openai_agents")
43
+ return "openai-agents";
44
+ if (h.startsWith("codex"))
45
+ return "codex"; // codex, codex_import, codex-exec
46
+ if (h.startsWith("cursor"))
47
+ return "cursor"; // cursor, cursor_import (adapter pending)
48
+ return "other";
49
+ }
50
+ //# sourceMappingURL=tool.js.map
@@ -19,6 +19,20 @@ export type PayloadClass = "metrics_only" | "redacted_structure" | "sanitized_sn
19
19
  export declare const CONTENT_BEARING_PAYLOAD_CLASSES: ReadonlySet<PayloadClass>;
20
20
  /** Honest token-source labels (mirror of `apps/api` `TokenSourceSchema`). Never upgraded. */
21
21
  export type TokenSource = "provider-reported" | "local-estimate" | "unknown";
22
+ /** Canonical coding-tool identity (re-exported from the source of truth in `./tool`). */
23
+ export type { ToolName } from "./tool.js";
24
+ import type { ToolName } from "./tool.js";
25
+ /**
26
+ * Content-free provider/tool attribution sent alongside a request. All fields are short
27
+ * identifiers (provider, tool, model name) — NEVER prompt/completion/trace content. The server
28
+ * records them on the content-free usage event so a workspace can be grouped by tool/provider/model.
29
+ */
30
+ export interface ProviderMetadata {
31
+ provider?: string;
32
+ runtime?: string;
33
+ tool?: ToolName;
34
+ model?: string;
35
+ }
22
36
  export interface Consent {
23
37
  upload_permitted: boolean;
24
38
  redacted: boolean;
@@ -37,10 +51,7 @@ export interface OptimizeRequest {
37
51
  trace: Trace;
38
52
  consent: Consent;
39
53
  payload_class?: PayloadClass;
40
- provider_metadata?: {
41
- provider?: string;
42
- runtime?: string;
43
- };
54
+ provider_metadata?: ProviderMetadata;
44
55
  token_usage: TokenUsage;
45
56
  evidence_labels?: Record<string, unknown>;
46
57
  labels?: {
@@ -0,0 +1,58 @@
1
+ import { type PolicyPreference, type PolicyPreferenceScope } from "./policy-preferences.js";
2
+ /** The exact §11a block-4 binary question. No scope menu — the scope is inferred, never asked. */
3
+ export declare const AUTO_APPLY_QUESTION_LINES: readonly string[];
4
+ /** A stored preference records these gate names (the founder's gate list from `auto-apply-gates`). */
5
+ export declare const AUTO_APPLY_PREFERENCE_GATES_REQUIRED: readonly string[];
6
+ export type AutoApplyAnswer = "yes" | "no";
7
+ /**
8
+ * Interpret a raw answer. DEFAULT IS NO: only an explicit `y` / `yes` (case-insensitive, trimmed) is
9
+ * `yes`; empty, `n`, `no`, undefined, or anything unrecognized is `no`. Never throws.
10
+ */
11
+ export declare function interpretAutoApplyAnswer(raw: string | null | undefined): AutoApplyAnswer;
12
+ /** Inputs from which the SAFEST scope is inferred — never a scope menu; the caller supplies facts. */
13
+ export interface AutoApplyScopeInputs {
14
+ /** REQUIRED. The workflow/tool this run belongs to (never global/cross-tool). */
15
+ tool?: string;
16
+ /** OPTIONAL. The repo, when detectable (content-free identifier). */
17
+ repo?: string;
18
+ /** REQUIRED. The policy type that was approved (content-free identifier). */
19
+ policyType?: string;
20
+ }
21
+ export interface AutoApplyOfferInput {
22
+ /** TRUE only when an explicit in-workflow approval already emitted an approved context. */
23
+ approvedInWorkflow: boolean;
24
+ /** The interpreted answer to the binary question (default no). */
25
+ answer: AutoApplyAnswer;
26
+ /** The facts from which the safest scope is inferred. */
27
+ scope: AutoApplyScopeInputs;
28
+ }
29
+ export type AutoApplyOfferDecision =
30
+ /** No approval happened, or the answer was no/default — save nothing (the common, safe path). */
31
+ {
32
+ action: "skip";
33
+ reason: string;
34
+ }
35
+ /** Approval + explicit yes, but the scope could not be inferred safely — fail-closed, save nothing. */
36
+ | {
37
+ action: "blocked";
38
+ problems: string[];
39
+ }
40
+ /** Approval + explicit yes + a safe scope — the CLI should upsert this preference (applies nothing). */
41
+ | {
42
+ action: "save";
43
+ scope: PolicyPreferenceScope;
44
+ preference: "auto-when-gates-pass";
45
+ gatesRequired: string[];
46
+ };
47
+ /**
48
+ * Resolve the binary offer into a DECISION. PURE — no I/O, no side effects. Enforces every rail:
49
+ * offer only after approval; default no; safest inferred scope; never global/cross-tool. Saving is
50
+ * left to the CLI boundary (`savePolicyPreference`); this never applies anything.
51
+ */
52
+ export declare function resolveAutoApplyOffer(input: AutoApplyOfferInput): AutoApplyOfferDecision;
53
+ /**
54
+ * The §11a block-5 saved-preference confirmation (scope · gates · undo hint). Honest: states the
55
+ * preference is stored and that every FUTURE application would be logged, evidence-labeled, undoable —
56
+ * it does NOT claim anything was applied. No wall-clock, no content.
57
+ */
58
+ export declare function formatSavedPreferenceConfirmation(preference: PolicyPreference): string;