@planckspace/cli 0.1.1 → 0.2.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 (190) hide show
  1. package/README.md +24 -1
  2. package/dist/anomaly/constants.d.ts +13 -0
  3. package/dist/anomaly/constants.d.ts.map +1 -0
  4. package/dist/anomaly/constants.js +13 -0
  5. package/dist/anomaly/constants.js.map +1 -0
  6. package/dist/anomaly/detector.d.ts +12 -0
  7. package/dist/anomaly/detector.d.ts.map +1 -0
  8. package/dist/anomaly/detector.js +98 -0
  9. package/dist/anomaly/detector.js.map +1 -0
  10. package/dist/anomaly/types.d.ts +19 -0
  11. package/dist/anomaly/types.d.ts.map +1 -0
  12. package/dist/anomaly/types.js +2 -0
  13. package/dist/anomaly/types.js.map +1 -0
  14. package/dist/commands/budget.d.ts +26 -0
  15. package/dist/commands/budget.d.ts.map +1 -0
  16. package/dist/commands/budget.js +91 -0
  17. package/dist/commands/budget.js.map +1 -0
  18. package/dist/commands/config.d.ts +4 -0
  19. package/dist/commands/config.d.ts.map +1 -0
  20. package/dist/commands/config.js +45 -0
  21. package/dist/commands/config.js.map +1 -0
  22. package/dist/commands/connect.d.ts +17 -0
  23. package/dist/commands/connect.d.ts.map +1 -0
  24. package/dist/commands/connect.js +191 -0
  25. package/dist/commands/connect.js.map +1 -0
  26. package/dist/commands/daemon.d.ts +8 -0
  27. package/dist/commands/daemon.d.ts.map +1 -0
  28. package/dist/commands/daemon.js +310 -0
  29. package/dist/commands/daemon.js.map +1 -0
  30. package/dist/commands/diagnose.d.ts +2 -0
  31. package/dist/commands/diagnose.d.ts.map +1 -0
  32. package/dist/commands/diagnose.js +81 -0
  33. package/dist/commands/diagnose.js.map +1 -0
  34. package/dist/commands/doctor.d.ts +2 -0
  35. package/dist/commands/doctor.d.ts.map +1 -0
  36. package/dist/commands/doctor.js +67 -0
  37. package/dist/commands/doctor.js.map +1 -0
  38. package/dist/commands/export.d.ts +40 -0
  39. package/dist/commands/export.d.ts.map +1 -0
  40. package/dist/commands/export.js +184 -0
  41. package/dist/commands/export.js.map +1 -0
  42. package/dist/commands/init.d.ts +3 -1
  43. package/dist/commands/init.d.ts.map +1 -1
  44. package/dist/commands/init.js +14 -1
  45. package/dist/commands/init.js.map +1 -1
  46. package/dist/commands/insights.d.ts +2 -0
  47. package/dist/commands/insights.d.ts.map +1 -0
  48. package/dist/commands/insights.js +71 -0
  49. package/dist/commands/insights.js.map +1 -0
  50. package/dist/commands/inspect.d.ts +2 -0
  51. package/dist/commands/inspect.d.ts.map +1 -0
  52. package/dist/commands/inspect.js +81 -0
  53. package/dist/commands/inspect.js.map +1 -0
  54. package/dist/commands/integrations.d.ts +2 -0
  55. package/dist/commands/integrations.d.ts.map +1 -0
  56. package/dist/commands/integrations.js +46 -0
  57. package/dist/commands/integrations.js.map +1 -0
  58. package/dist/commands/login.d.ts +1 -1
  59. package/dist/commands/login.d.ts.map +1 -1
  60. package/dist/commands/login.js +45 -6
  61. package/dist/commands/login.js.map +1 -1
  62. package/dist/commands/metrics.d.ts +6 -0
  63. package/dist/commands/metrics.d.ts.map +1 -0
  64. package/dist/commands/metrics.js +85 -0
  65. package/dist/commands/metrics.js.map +1 -0
  66. package/dist/commands/reconcile.d.ts +2 -0
  67. package/dist/commands/reconcile.d.ts.map +1 -0
  68. package/dist/commands/reconcile.js +63 -0
  69. package/dist/commands/reconcile.js.map +1 -0
  70. package/dist/commands/scan.d.ts.map +1 -1
  71. package/dist/commands/scan.js +53 -10
  72. package/dist/commands/scan.js.map +1 -1
  73. package/dist/commands/status.d.ts +1 -1
  74. package/dist/commands/status.d.ts.map +1 -1
  75. package/dist/commands/status.js +84 -14
  76. package/dist/commands/status.js.map +1 -1
  77. package/dist/commands/subscription.d.ts +15 -0
  78. package/dist/commands/subscription.d.ts.map +1 -0
  79. package/dist/commands/subscription.js +62 -0
  80. package/dist/commands/subscription.js.map +1 -0
  81. package/dist/commands/sync.d.ts +1 -1
  82. package/dist/commands/sync.d.ts.map +1 -1
  83. package/dist/commands/sync.js +77 -10
  84. package/dist/commands/sync.js.map +1 -1
  85. package/dist/commands/value.d.ts +5 -0
  86. package/dist/commands/value.d.ts.map +1 -0
  87. package/dist/commands/value.js +95 -0
  88. package/dist/commands/value.js.map +1 -0
  89. package/dist/config.d.ts +28 -3
  90. package/dist/config.d.ts.map +1 -1
  91. package/dist/config.js +9 -1
  92. package/dist/config.js.map +1 -1
  93. package/dist/correlate.d.ts +7 -0
  94. package/dist/correlate.d.ts.map +1 -1
  95. package/dist/correlate.js +102 -15
  96. package/dist/correlate.js.map +1 -1
  97. package/dist/daemon/daemon.d.ts +2 -0
  98. package/dist/daemon/daemon.d.ts.map +1 -0
  99. package/dist/daemon/daemon.js +188 -0
  100. package/dist/daemon/daemon.js.map +1 -0
  101. package/dist/daemon/daemonState.d.ts +25 -0
  102. package/dist/daemon/daemonState.d.ts.map +1 -0
  103. package/dist/daemon/daemonState.js +82 -0
  104. package/dist/daemon/daemonState.js.map +1 -0
  105. package/dist/daemon/logger.d.ts +7 -0
  106. package/dist/daemon/logger.d.ts.map +1 -0
  107. package/dist/daemon/logger.js +61 -0
  108. package/dist/daemon/logger.js.map +1 -0
  109. package/dist/daemon/syncLoop.d.ts +38 -0
  110. package/dist/daemon/syncLoop.d.ts.map +1 -0
  111. package/dist/daemon/syncLoop.js +119 -0
  112. package/dist/daemon/syncLoop.js.map +1 -0
  113. package/dist/daemon/watcher.d.ts +26 -0
  114. package/dist/daemon/watcher.d.ts.map +1 -0
  115. package/dist/daemon/watcher.js +187 -0
  116. package/dist/daemon/watcher.js.map +1 -0
  117. package/dist/db/store.d.ts +123 -2
  118. package/dist/db/store.d.ts.map +1 -1
  119. package/dist/db/store.js +397 -11
  120. package/dist/db/store.js.map +1 -1
  121. package/dist/detectors/cache-gap.d.ts +3 -0
  122. package/dist/detectors/cache-gap.d.ts.map +1 -0
  123. package/dist/detectors/cache-gap.js +70 -0
  124. package/dist/detectors/cache-gap.js.map +1 -0
  125. package/dist/detectors/context-bloat.d.ts +3 -0
  126. package/dist/detectors/context-bloat.d.ts.map +1 -0
  127. package/dist/detectors/context-bloat.js +68 -0
  128. package/dist/detectors/context-bloat.js.map +1 -0
  129. package/dist/detectors/fileTokens.d.ts +3 -0
  130. package/dist/detectors/fileTokens.d.ts.map +1 -0
  131. package/dist/detectors/fileTokens.js +12 -0
  132. package/dist/detectors/fileTokens.js.map +1 -0
  133. package/dist/detectors/index.d.ts +20 -0
  134. package/dist/detectors/index.d.ts.map +1 -0
  135. package/dist/detectors/index.js +41 -0
  136. package/dist/detectors/index.js.map +1 -0
  137. package/dist/detectors/model-routing.d.ts +3 -0
  138. package/dist/detectors/model-routing.d.ts.map +1 -0
  139. package/dist/detectors/model-routing.js +71 -0
  140. package/dist/detectors/model-routing.js.map +1 -0
  141. package/dist/detectors/repeat-read.d.ts +3 -0
  142. package/dist/detectors/repeat-read.d.ts.map +1 -0
  143. package/dist/detectors/repeat-read.js +69 -0
  144. package/dist/detectors/repeat-read.js.map +1 -0
  145. package/dist/detectors/seat-efficiency.d.ts +4 -0
  146. package/dist/detectors/seat-efficiency.d.ts.map +1 -0
  147. package/dist/detectors/seat-efficiency.js +86 -0
  148. package/dist/detectors/seat-efficiency.js.map +1 -0
  149. package/dist/detectors/types.d.ts +46 -0
  150. package/dist/detectors/types.d.ts.map +1 -0
  151. package/dist/detectors/types.js +2 -0
  152. package/dist/detectors/types.js.map +1 -0
  153. package/dist/health.d.ts +59 -0
  154. package/dist/health.d.ts.map +1 -0
  155. package/dist/health.js +106 -0
  156. package/dist/health.js.map +1 -0
  157. package/dist/index.js +389 -5
  158. package/dist/index.js.map +1 -1
  159. package/dist/metrics.d.ts +29 -0
  160. package/dist/metrics.d.ts.map +1 -0
  161. package/dist/metrics.js +205 -0
  162. package/dist/metrics.js.map +1 -0
  163. package/dist/scrapers/claudeCode.d.ts +1 -0
  164. package/dist/scrapers/claudeCode.d.ts.map +1 -1
  165. package/dist/scrapers/claudeCode.js +43 -13
  166. package/dist/scrapers/claudeCode.js.map +1 -1
  167. package/dist/scrapers/cursor.d.ts +3 -2
  168. package/dist/scrapers/cursor.d.ts.map +1 -1
  169. package/dist/scrapers/cursor.js +56 -16
  170. package/dist/scrapers/cursor.js.map +1 -1
  171. package/dist/scrapers/jetbrains.d.ts +15 -0
  172. package/dist/scrapers/jetbrains.d.ts.map +1 -0
  173. package/dist/scrapers/jetbrains.js +232 -0
  174. package/dist/scrapers/jetbrains.js.map +1 -0
  175. package/dist/scrapers/types.d.ts +4 -1
  176. package/dist/scrapers/types.d.ts.map +1 -1
  177. package/dist/scrapers/windsurf.d.ts +3 -2
  178. package/dist/scrapers/windsurf.d.ts.map +1 -1
  179. package/dist/scrapers/windsurf.js +25 -9
  180. package/dist/scrapers/windsurf.js.map +1 -1
  181. package/dist/sync/payload.d.ts +4 -5
  182. package/dist/sync/payload.d.ts.map +1 -1
  183. package/dist/sync/payload.js +88 -7
  184. package/dist/sync/payload.js.map +1 -1
  185. package/dist/sync/syncEngine.d.ts +19 -3
  186. package/dist/sync/syncEngine.d.ts.map +1 -1
  187. package/dist/sync/syncEngine.js +116 -10
  188. package/dist/sync/syncEngine.js.map +1 -1
  189. package/install.sh +27 -10
  190. package/package.json +43 -42
package/README.md CHANGED
@@ -8,7 +8,7 @@ PlanckSpace CLI — sync AI token usage from Claude Code, Cursor, and Windsurf t
8
8
  curl -fsSL https://planckspace.dev/install | sh
9
9
  ```
10
10
 
11
- With an invite token:
11
+ With an invite token (skips the manual login step):
12
12
 
13
13
  ```sh
14
14
  curl -fsSL https://planckspace.dev/install | sh -s -- --token pk_live_xxx
@@ -22,6 +22,22 @@ npm install -g @planckspace/cli
22
22
 
23
23
  See [docs/INSTALL.md](docs/INSTALL.md) for manual steps and troubleshooting.
24
24
 
25
+ ## VS Code extension
26
+
27
+ Install the companion extension to see spend, insights, and skill recommendations right in your editor:
28
+
29
+ - **VS Code Marketplace** — search "Planckspace" in the Extensions view, or run:
30
+ ```sh
31
+ code --install-extension planckspace.planckspace-extension
32
+ ```
33
+ - **Open VSX** (Cursor / Windsurf / VS Codium): [open-vsx.org/extension/planckspace/planckspace-extension](https://open-vsx.org/extension/planckspace/planckspace-extension)
34
+ - **Headless / CI**:
35
+ ```sh
36
+ code --install-extension planckspace.planckspace-extension
37
+ # or install from VSIX:
38
+ code --install-extension planckspace-extension-win32-x64-0.2.0.vsix
39
+ ```
40
+
25
41
  ## Commands
26
42
 
27
43
  | Command | Description |
@@ -56,6 +72,13 @@ planck scan --sync
56
72
 
57
73
  See [docs/COVERAGE.md](docs/COVERAGE.md) for per-tool capture details.
58
74
 
75
+ ## What's new in 0.2.0
76
+
77
+ - **Cost insights** — the CLI now detects patterns that inflate spend (large CLAUDE.md, low cache hit rates, repeated context) and writes them to `~/.planckspace/local.db`. The VS Code extension surfaces these with estimated monthly savings.
78
+ - **Audit scoring** — `planck status` reports a workspace configuration score across five dimensions: CLAUDE.md token budget, prompt-cache hit rate, hooks, installed skills, and MCP servers.
79
+ - **Improved Windsurf support** — schema detection updated for recent Windsurf session log changes.
80
+ - **Anomaly detection** — the CLI flags sessions with unusually high token counts or cost spikes.
81
+
59
82
  ## Supported editors
60
83
 
61
84
  | Editor | Sessions | Tokens / cost | Notes |
@@ -0,0 +1,13 @@
1
+ /** Tunable thresholds for anomaly detection and budget alerting. */
2
+ /** Daily cost must exceed this multiple of the 7-day median to fire a daily spike. */
3
+ export declare const DAILY_SPIKE_RATIO = 3;
4
+ /** Daily cost must also exceed this absolute floor (USD) to fire a daily spike. */
5
+ export declare const DAILY_SPIKE_MIN_USD = 20;
6
+ /** Session cost must exceed this multiple of the developer's p90 to fire a session spike. */
7
+ export declare const SESSION_SPIKE_RATIO = 5;
8
+ /** Minimum number of sessions a developer must have in the trailing 30 days before a p90
9
+ * baseline is meaningful enough to fire session-level anomalies. */
10
+ export declare const SESSION_P90_MIN_SAMPLES = 5;
11
+ /** Fraction of monthly budget consumed before a warning is printed at the end of scan. */
12
+ export declare const BUDGET_WARN_THRESHOLD = 0.8;
13
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../src/anomaly/constants.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,sFAAsF;AACtF,eAAO,MAAM,iBAAiB,IAAI,CAAC;AAEnC,mFAAmF;AACnF,eAAO,MAAM,mBAAmB,KAAK,CAAC;AAEtC,6FAA6F;AAC7F,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC;qEACqE;AACrE,eAAO,MAAM,uBAAuB,IAAI,CAAC;AAEzC,0FAA0F;AAC1F,eAAO,MAAM,qBAAqB,MAAO,CAAC"}
@@ -0,0 +1,13 @@
1
+ /** Tunable thresholds for anomaly detection and budget alerting. */
2
+ /** Daily cost must exceed this multiple of the 7-day median to fire a daily spike. */
3
+ export const DAILY_SPIKE_RATIO = 3;
4
+ /** Daily cost must also exceed this absolute floor (USD) to fire a daily spike. */
5
+ export const DAILY_SPIKE_MIN_USD = 20;
6
+ /** Session cost must exceed this multiple of the developer's p90 to fire a session spike. */
7
+ export const SESSION_SPIKE_RATIO = 5;
8
+ /** Minimum number of sessions a developer must have in the trailing 30 days before a p90
9
+ * baseline is meaningful enough to fire session-level anomalies. */
10
+ export const SESSION_P90_MIN_SAMPLES = 5;
11
+ /** Fraction of monthly budget consumed before a warning is printed at the end of scan. */
12
+ export const BUDGET_WARN_THRESHOLD = 0.80;
13
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../../src/anomaly/constants.ts"],"names":[],"mappings":"AAAA,oEAAoE;AAEpE,sFAAsF;AACtF,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAEnC,mFAAmF;AACnF,MAAM,CAAC,MAAM,mBAAmB,GAAG,EAAE,CAAC;AAEtC,6FAA6F;AAC7F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,CAAC;AAErC;qEACqE;AACrE,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,CAAC;AAEzC,0FAA0F;AAC1F,MAAM,CAAC,MAAM,qBAAqB,GAAG,IAAI,CAAC"}
@@ -0,0 +1,12 @@
1
+ import type { AnomalyEvent } from "./types.js";
2
+ /**
3
+ * Detect anomalous cost events.
4
+ *
5
+ * Two detectors run:
6
+ * 1. Daily cost spike — per repo, trailing 7-day median vs most-recent day.
7
+ * 2. Session cost spike — per developer, p90 over 30 days vs individual session.
8
+ *
9
+ * Pass `now` for deterministic testing (defaults to current time).
10
+ */
11
+ export declare function detectAnomalies(now?: Date): AnomalyEvent[];
12
+ //# sourceMappingURL=detector.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.d.ts","sourceRoot":"","sources":["../../src/anomaly/detector.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA8B/C;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,GAAG,OAAa,GAAG,YAAY,EAAE,CA0EhE"}
@@ -0,0 +1,98 @@
1
+ import { getDailySessionCosts, getSessionsForAnomalyDetection, } from "../db/store.js";
2
+ import { DAILY_SPIKE_RATIO, DAILY_SPIKE_MIN_USD, SESSION_SPIKE_RATIO, SESSION_P90_MIN_SAMPLES, } from "./constants.js";
3
+ function median(sorted) {
4
+ if (sorted.length === 0)
5
+ return 0;
6
+ const mid = Math.floor(sorted.length / 2);
7
+ return sorted.length % 2 === 0
8
+ ? (sorted[mid - 1] + sorted[mid]) / 2
9
+ : sorted[mid];
10
+ }
11
+ function percentile90(sorted) {
12
+ if (sorted.length === 0)
13
+ return 0;
14
+ const idx = Math.ceil(0.9 * sorted.length) - 1;
15
+ return sorted[Math.max(0, Math.min(idx, sorted.length - 1))];
16
+ }
17
+ /**
18
+ * Detect anomalous cost events.
19
+ *
20
+ * Two detectors run:
21
+ * 1. Daily cost spike — per repo, trailing 7-day median vs most-recent day.
22
+ * 2. Session cost spike — per developer, p90 over 30 days vs individual session.
23
+ *
24
+ * Pass `now` for deterministic testing (defaults to current time).
25
+ */
26
+ export function detectAnomalies(now = new Date()) {
27
+ const events = [];
28
+ const detectedAt = now.toISOString();
29
+ // ── 1. Daily cost spike per repo ──────────────────────────────────────────
30
+ const eightDaysAgo = new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString();
31
+ const dailyRows = getDailySessionCosts(eightDaysAgo);
32
+ // Group by repo, rows already ordered by day ASC from the query.
33
+ const byRepo = new Map();
34
+ for (const r of dailyRows) {
35
+ const key = r.repoName ?? null;
36
+ if (!byRepo.has(key))
37
+ byRepo.set(key, []);
38
+ byRepo.get(key).push({ day: r.day, dailyCost: r.dailyCost });
39
+ }
40
+ for (const [repoName, days] of byRepo) {
41
+ if (days.length < 2)
42
+ continue; // Need at least one baseline day + one current day.
43
+ const mostRecent = days[days.length - 1];
44
+ const baselineDays = days.slice(0, -1);
45
+ const sorted = baselineDays.map((d) => d.dailyCost).sort((a, b) => a - b);
46
+ const base = median(sorted);
47
+ if (base <= 0)
48
+ continue;
49
+ const ratio = mostRecent.dailyCost / base;
50
+ if (ratio > DAILY_SPIKE_RATIO && mostRecent.dailyCost > DAILY_SPIKE_MIN_USD) {
51
+ events.push({
52
+ id: `daily:${repoName ?? "unknown"}:${mostRecent.day}`,
53
+ type: "daily_cost_spike",
54
+ repoName,
55
+ sessionId: null,
56
+ costUsd: mostRecent.dailyCost,
57
+ baselineUsd: base,
58
+ ratio,
59
+ detectedAt,
60
+ });
61
+ }
62
+ }
63
+ // ── 2. Per-session cost spike (vs developer p90) ───────────────────────────
64
+ const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
65
+ const sessionRows = getSessionsForAnomalyDetection(thirtyDaysAgo);
66
+ // Group by developer email; rows already ordered by email, costUsd ASC.
67
+ const byDeveloper = new Map();
68
+ for (const r of sessionRows) {
69
+ if (!byDeveloper.has(r.gitAuthorEmail))
70
+ byDeveloper.set(r.gitAuthorEmail, []);
71
+ byDeveloper.get(r.gitAuthorEmail).push(r);
72
+ }
73
+ for (const sessions of byDeveloper.values()) {
74
+ if (sessions.length < SESSION_P90_MIN_SAMPLES)
75
+ continue;
76
+ const sortedCosts = sessions.map((s) => s.costUsd); // already sorted ASC from the query
77
+ const p90 = percentile90(sortedCosts);
78
+ if (p90 <= 0)
79
+ continue;
80
+ for (const s of sessions) {
81
+ const ratio = s.costUsd / p90;
82
+ if (ratio > SESSION_SPIKE_RATIO) {
83
+ events.push({
84
+ id: `session:${s.id}`,
85
+ type: "session_cost_spike",
86
+ repoName: s.repoName,
87
+ sessionId: s.id,
88
+ costUsd: s.costUsd,
89
+ baselineUsd: p90,
90
+ ratio,
91
+ detectedAt,
92
+ });
93
+ }
94
+ }
95
+ }
96
+ return events;
97
+ }
98
+ //# sourceMappingURL=detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.js","sourceRoot":"","sources":["../../src/anomaly/detector.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ export type AnomalyType = "daily_cost_spike" | "session_cost_spike";
2
+ export type AnomalyEvent = {
3
+ /** Stable ID used for deduplication across consecutive scans:
4
+ * daily: `daily:<repoName>:<YYYY-MM-DD>`
5
+ * session: `session:<sessionId>` */
6
+ id: string;
7
+ type: AnomalyType;
8
+ repoName: string | null;
9
+ /** Only set for session-level anomalies. */
10
+ sessionId: string | null;
11
+ /** The cost that tripped the threshold. */
12
+ costUsd: number;
13
+ /** Baseline the cost was compared against (7-day median per day, or developer p90). */
14
+ baselineUsd: number;
15
+ /** costUsd / baselineUsd */
16
+ ratio: number;
17
+ detectedAt: string;
18
+ };
19
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/anomaly/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,WAAW,GAAG,kBAAkB,GAAG,oBAAoB,CAAC;AAEpE,MAAM,MAAM,YAAY,GAAG;IACzB;;yCAEqC;IACrC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,4CAA4C;IAC5C,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,uFAAuF;IACvF,WAAW,EAAE,MAAM,CAAC;IACpB,4BAA4B;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/anomaly/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import { type Budget } from "../config.js";
2
+ export type BudgetSetOptions = {
3
+ scope: string;
4
+ name: string;
5
+ monthly: number;
6
+ };
7
+ export type BudgetRemoveOptions = {
8
+ scope: string;
9
+ name: string;
10
+ };
11
+ export declare function runBudgetSet(opts: BudgetSetOptions): void;
12
+ export declare function runBudgetList(): void;
13
+ export declare function runBudgetRemove(opts: BudgetRemoveOptions): void;
14
+ /**
15
+ * Compute which budgets are at or over the warn threshold and return
16
+ * human-readable warning strings.
17
+ *
18
+ * `monthStart` is the ISO timestamp of the first moment of the current billing month.
19
+ */
20
+ export declare function getBudgetWarnings(budgets: Budget[], monthStart: string): string[];
21
+ /**
22
+ * Returns a summary line for each budget showing spend vs cap and burn %.
23
+ * Intended for use in `planck status`.
24
+ */
25
+ export declare function getBudgetBurnLines(budgets: Budget[], monthStart: string): string[];
26
+ //# sourceMappingURL=budget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.d.ts","sourceRoot":"","sources":["../../src/commands/budget.ts"],"names":[],"mappings":"AAAA,OAAO,EAA2B,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAIpE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAIF,wBAAgB,YAAY,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,CA4BzD;AAED,wBAAgB,aAAa,IAAI,IAAI,CAiBpC;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAc/D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAkBjF;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,EAAE,CAiBlF"}
@@ -0,0 +1,91 @@
1
+ import { readConfig, writeConfig } from "../config.js";
2
+ import { getMonthlyCosts } from "../db/store.js";
3
+ import { BUDGET_WARN_THRESHOLD } from "../anomaly/constants.js";
4
+ const VALID_SCOPES = new Set(["repo", "workspace"]);
5
+ export function runBudgetSet(opts) {
6
+ if (!VALID_SCOPES.has(opts.scope)) {
7
+ throw new Error(`--scope must be one of: ${[...VALID_SCOPES].join(", ")}`);
8
+ }
9
+ if (opts.monthly <= 0) {
10
+ throw new Error("--monthly must be a positive number");
11
+ }
12
+ const config = readConfig();
13
+ const existing = config.budgets.findIndex((b) => b.scope === opts.scope && b.name === opts.name);
14
+ const entry = {
15
+ scope: opts.scope,
16
+ name: opts.name,
17
+ monthly: opts.monthly,
18
+ };
19
+ if (existing >= 0) {
20
+ config.budgets[existing] = entry;
21
+ console.log(`Updated budget: ${opts.scope} "${opts.name}" → $${opts.monthly.toFixed(2)}/month`);
22
+ }
23
+ else {
24
+ config.budgets.push(entry);
25
+ console.log(`Set budget: ${opts.scope} "${opts.name}" → $${opts.monthly.toFixed(2)}/month`);
26
+ }
27
+ writeConfig(config);
28
+ }
29
+ export function runBudgetList() {
30
+ const { budgets } = readConfig();
31
+ if (budgets.length === 0) {
32
+ console.log("No budgets set. Use `planck budget set --scope repo --name <repo> --monthly <amount>`");
33
+ return;
34
+ }
35
+ console.log("Configured budgets:");
36
+ console.log("─────────────────────────────────────");
37
+ for (const b of budgets) {
38
+ console.log(` ${b.scope.padEnd(10)} ${b.name.padEnd(30)} $${b.monthly.toFixed(2)}/month`);
39
+ }
40
+ }
41
+ export function runBudgetRemove(opts) {
42
+ const config = readConfig();
43
+ const before = config.budgets.length;
44
+ config.budgets = config.budgets.filter((b) => !(b.scope === opts.scope && b.name === opts.name));
45
+ if (config.budgets.length === before) {
46
+ console.log(`No budget found for scope=${opts.scope} name="${opts.name}"`);
47
+ return;
48
+ }
49
+ writeConfig(config);
50
+ console.log(`Removed budget: ${opts.scope} "${opts.name}"`);
51
+ }
52
+ /**
53
+ * Compute which budgets are at or over the warn threshold and return
54
+ * human-readable warning strings.
55
+ *
56
+ * `monthStart` is the ISO timestamp of the first moment of the current billing month.
57
+ */
58
+ export function getBudgetWarnings(budgets, monthStart) {
59
+ if (budgets.length === 0)
60
+ return [];
61
+ const { byRepo, total } = getMonthlyCosts(monthStart);
62
+ const warnings = [];
63
+ for (const b of budgets) {
64
+ const spent = b.scope === "repo" ? (byRepo.get(b.name) ?? 0) : total;
65
+ const pct = spent / b.monthly;
66
+ if (pct >= BUDGET_WARN_THRESHOLD) {
67
+ warnings.push(`WARNING: "${b.name}" (${b.scope}) is at ${(pct * 100).toFixed(0)}% of ` +
68
+ `$${b.monthly.toFixed(2)}/month budget ($${spent.toFixed(2)} spent so far this month)`);
69
+ }
70
+ }
71
+ return warnings;
72
+ }
73
+ /**
74
+ * Returns a summary line for each budget showing spend vs cap and burn %.
75
+ * Intended for use in `planck status`.
76
+ */
77
+ export function getBudgetBurnLines(budgets, monthStart) {
78
+ if (budgets.length === 0)
79
+ return [];
80
+ const { byRepo, total } = getMonthlyCosts(monthStart);
81
+ const lines = [];
82
+ for (const b of budgets) {
83
+ const spent = b.scope === "repo" ? (byRepo.get(b.name) ?? 0) : total;
84
+ const pct = spent / b.monthly;
85
+ const bar = pct >= BUDGET_WARN_THRESHOLD ? " ⚠" : "";
86
+ lines.push(` ${b.scope.padEnd(10)} ${b.name.padEnd(30)} ` +
87
+ `$${spent.toFixed(2)} / $${b.monthly.toFixed(2)} (${(pct * 100).toFixed(0)}%)${bar}`);
88
+ }
89
+ return lines;
90
+ }
91
+ //# sourceMappingURL=budget.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"budget.js","sourceRoot":"","sources":["../../src/commands/budget.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export declare function runConfigSet(key: string, value: string): void;
2
+ export declare function runConfigGet(key: string): void;
3
+ export declare function runConfigShow(): void;
4
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAYA,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAe7D;AAED,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAQ9C;AAED,wBAAgB,aAAa,IAAI,IAAI,CAepC"}
@@ -0,0 +1,45 @@
1
+ import { readConfig, writeConfig, CONFIG_PATH } from "../config.js";
2
+ // Keys a developer is allowed to set via `planck config set`.
3
+ // Structured arrays (budgets, subscriptions, teamMappings) have their own commands.
4
+ const SETTABLE_KEYS = ["apiUrl", "privacyMode"];
5
+ function isSettable(key) {
6
+ return SETTABLE_KEYS.includes(key);
7
+ }
8
+ export function runConfigSet(key, value) {
9
+ if (!isSettable(key)) {
10
+ console.error(`Unknown or non-settable key: "${key}"\n` +
11
+ `Settable keys: ${SETTABLE_KEYS.join(", ")}\n` +
12
+ `(Use dedicated commands for budgets / subscriptions / teamMappings)`);
13
+ process.exit(1);
14
+ }
15
+ const config = readConfig();
16
+ const updated = { ...config, [key]: value };
17
+ writeConfig(updated);
18
+ console.log(`${key} = ${value}`);
19
+ console.log(`Saved to ${CONFIG_PATH}`);
20
+ }
21
+ export function runConfigGet(key) {
22
+ const config = readConfig();
23
+ if (!(key in config)) {
24
+ console.error(`Unknown config key: "${key}"`);
25
+ process.exit(1);
26
+ }
27
+ const val = config[key];
28
+ console.log(val === null || val === undefined ? "(not set)" : String(val));
29
+ }
30
+ export function runConfigShow() {
31
+ const config = readConfig();
32
+ const token = config.token;
33
+ const maskedToken = token ? `...${token.slice(-4)}` : "(not set)";
34
+ console.log("PlanckSpace Config");
35
+ console.log("─────────────────────────────────────");
36
+ console.log(`File : ${CONFIG_PATH}`);
37
+ console.log(`apiUrl : ${config.apiUrl}`);
38
+ console.log(`workspaceId : ${config.workspaceId ?? "(not set)"}`);
39
+ console.log(`workspaceName : ${config.workspaceName ?? "(not set)"}`);
40
+ console.log(`token : ${maskedToken}`);
41
+ console.log(`privacyMode : ${config.privacyMode}`);
42
+ console.log(`budgets : ${config.budgets.length}`);
43
+ console.log(`subscriptions : ${config.subscriptions.length}`);
44
+ }
45
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/commands/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGpE,8DAA8D;AAC9D,oFAAoF;AACpF,MAAM,aAAa,GAAG,CAAC,QAAQ,EAAE,aAAa,CAAU,CAAC;AAGzD,SAAS,UAAU,CAAC,GAAW;IAC7B,OAAQ,aAAmC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAC5D,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW,EAAE,KAAa;IACrD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,KAAK,CACX,iCAAiC,GAAG,KAAK;YACvC,kBAAkB,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI;YAC9C,qEAAqE,CACxE,CAAC;QACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,OAAO,GAAgB,EAAE,GAAG,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,CAAC;IACzD,WAAW,CAAC,OAAO,CAAC,CAAC;IACrB,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,MAAM,KAAK,EAAE,CAAC,CAAC;IACjC,OAAO,CAAC,GAAG,CAAC,YAAY,WAAW,EAAE,CAAC,CAAC;AACzC,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,GAAW;IACtC,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,IAAI,CAAC,CAAC,GAAG,IAAI,MAAM,CAAC,EAAE,CAAC;QACrB,OAAO,CAAC,KAAK,CAAC,wBAAwB,GAAG,GAAG,CAAC,CAAC;QAC9C,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;IACD,MAAM,GAAG,GAAG,MAAM,CAAC,GAAwB,CAAC,CAAC;IAC7C,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,IAAI,IAAI,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,MAAM,MAAM,GAAG,UAAU,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;IAC3B,MAAM,WAAW,GAAG,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC;IAElE,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;IAClC,OAAO,CAAC,GAAG,CAAC,uCAAuC,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC;IAChD,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,WAAW,IAAI,WAAW,EAAE,CAAC,CAAC;IACpE,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,aAAa,IAAI,WAAW,EAAE,CAAC,CAAC;IACtE,OAAO,CAAC,GAAG,CAAC,mBAAmB,WAAW,EAAE,CAAC,CAAC;IAC9C,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,WAAW,EAAE,CAAC,CAAC;IACrD,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACxD,OAAO,CAAC,GAAG,CAAC,mBAAmB,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,CAAC;AAChE,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Strip anything that looks like an API key/token from text before it is printed.
3
+ * Provider rejection errors are relayed by our backend (a 422 whose body embeds,
4
+ * e.g. OpenAI's "Incorrect API key provided: sk-..."). This is the last line of
5
+ * defence for the privacy guarantee: no credential value reaches a log line.
6
+ */
7
+ export declare function redactSecrets(text: string): string;
8
+ export declare function runConnectAnthropic(opts: {
9
+ adminKey: string;
10
+ }): Promise<void>;
11
+ export declare function runConnectOpenAI(opts: {
12
+ apiKey: string;
13
+ }): Promise<void>;
14
+ export declare function runConnectBedrock(opts: {
15
+ roleArn: string;
16
+ }): Promise<void>;
17
+ //# sourceMappingURL=connect.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"connect.d.ts","sourceRoot":"","sources":["../../src/commands/connect.ts"],"names":[],"mappings":"AAYA;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAMlD;AAwHD,wBAAsB,mBAAmB,CAAC,IAAI,EAAE;IAC9C,QAAQ,EAAE,MAAM,CAAC;CAClB,GAAG,OAAO,CAAC,IAAI,CAAC,CAiBhB;AAID,wBAAsB,gBAAgB,CAAC,IAAI,EAAE;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAiB9E;AAiBD,wBAAsB,iBAAiB,CAAC,IAAI,EAAE;IAC5C,OAAO,EAAE,MAAM,CAAC;CACjB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8BhB"}
@@ -0,0 +1,191 @@
1
+ import { readConfig, resolveApiUrl } from "../config.js";
2
+ // Backend (planckspace-backend) enforces exactly 12 account digits; match it so
3
+ // the CLI rejects the same malformed ARNs locally before any round-trip.
4
+ const BEDROCK_ARN_RE = /^arn:aws:iam::\d{12}:role\/.+$/;
5
+ const PROVIDER_LABEL = {
6
+ anthropic: "Anthropic",
7
+ openai: "OpenAI",
8
+ bedrock: "AWS",
9
+ };
10
+ /**
11
+ * Strip anything that looks like an API key/token from text before it is printed.
12
+ * Provider rejection errors are relayed by our backend (a 422 whose body embeds,
13
+ * e.g. OpenAI's "Incorrect API key provided: sk-..."). This is the last line of
14
+ * defence for the privacy guarantee: no credential value reaches a log line.
15
+ */
16
+ export function redactSecrets(text) {
17
+ return text
18
+ .replace(/sk-ant-admin-[A-Za-z0-9_-]+/g, "sk-ant-admin-***")
19
+ .replace(/sk-ant-[A-Za-z0-9_-]+/g, "sk-ant-***")
20
+ .replace(/sk-[A-Za-z0-9_-]+/g, "sk-***")
21
+ .replace(/Bearer\s+[A-Za-z0-9_.\-]+/gi, "Bearer ***");
22
+ }
23
+ /** Pull a concise, secret-free reason out of a provider/backend error body. */
24
+ function summarizeBody(bodyText) {
25
+ let reason = bodyText.trim();
26
+ try {
27
+ const parsed = JSON.parse(bodyText);
28
+ const err = parsed.error;
29
+ if (typeof err === "string")
30
+ reason = err;
31
+ else if (err && typeof err === "object" && typeof err.message === "string")
32
+ reason = err.message;
33
+ else if (typeof parsed.message === "string")
34
+ reason = parsed.message;
35
+ }
36
+ catch {
37
+ // Not JSON — fall back to the raw (trimmed) text.
38
+ }
39
+ let out = typeof reason === "string" ? reason : JSON.stringify(reason);
40
+ if (out.length > 400)
41
+ out = `${out.slice(0, 400)}…`;
42
+ return redactSecrets(out);
43
+ }
44
+ /** Print a user-facing error and flag a non-zero exit WITHOUT killing the event
45
+ * loop mid-fetch (process.exit() during an open undici socket aborts on Windows). */
46
+ function fail(message) {
47
+ console.error(message);
48
+ process.exitCode = 1;
49
+ }
50
+ async function requireAuth() {
51
+ const config = readConfig();
52
+ if (!config.token) {
53
+ fail("Not connected. Run `planck login <token>` first.");
54
+ return null;
55
+ }
56
+ return {
57
+ token: config.token,
58
+ apiUrl: resolveApiUrl(),
59
+ workspaceId: config.workspaceId ?? null,
60
+ };
61
+ }
62
+ /**
63
+ * POST credentials to the backend over TLS. The backend stores them encrypted and
64
+ * forwards them to the real provider for validation. We never validate against the
65
+ * provider ourselves — that keeps the credential's only network destination our
66
+ * own backend, and makes provider rejections come back attributed (HTTP 422).
67
+ *
68
+ * Returns the outcome; never throws, never prints the credential. Returns null
69
+ * only on a network-level failure (already reported).
70
+ */
71
+ async function postIntegration(apiUrl, token, provider, body) {
72
+ try {
73
+ const res = await fetch(`${apiUrl}/api/integrations/${provider}`, {
74
+ method: "POST",
75
+ headers: {
76
+ "Content-Type": "application/json",
77
+ Authorization: `Bearer ${token}`,
78
+ },
79
+ body: JSON.stringify(body),
80
+ });
81
+ const bodyText = await res.text().catch(() => "");
82
+ return { ok: res.ok, status: res.status, bodyText };
83
+ }
84
+ catch (err) {
85
+ fail(`Could not reach backend: ${redactSecrets(String(err))}`);
86
+ return null;
87
+ }
88
+ }
89
+ /**
90
+ * Translate a backend response into a user-facing, provider-attributed message.
91
+ * Returns true only on success (2xx). The status taxonomy matters:
92
+ * - 422 → the provider rejected the credential (the expected "bad key" path).
93
+ * - 401 → our backend did not accept the caller's auth (a real regression).
94
+ * - 403 → caller authenticated but lacks the admin role for integrations.
95
+ * - 4xx/5xx → a backend-side problem, surfaced as such (not blamed on the provider).
96
+ */
97
+ function reportConnectResult(provider, result) {
98
+ if (result.ok)
99
+ return true;
100
+ const label = PROVIDER_LABEL[provider];
101
+ const reason = summarizeBody(result.bodyText);
102
+ switch (result.status) {
103
+ case 422:
104
+ fail(`${label} rejected the credential (validation failed at ${label}): ${reason}\n` +
105
+ "Nothing was connected.");
106
+ break;
107
+ case 401:
108
+ fail(`Backend authentication failed (401): ${reason}`);
109
+ break;
110
+ case 403:
111
+ fail(`Backend denied the request (403): ${reason} — your token may lack the admin role.`);
112
+ break;
113
+ case 400:
114
+ fail(`Backend rejected the request (400): ${reason}`);
115
+ break;
116
+ default:
117
+ fail(`Backend error (status ${result.status}): ${reason}`);
118
+ }
119
+ return false;
120
+ }
121
+ // ─── anthropic ──────────────────────────────────────────────────────────────
122
+ export async function runConnectAnthropic(opts) {
123
+ const auth = await requireAuth();
124
+ if (!auth)
125
+ return;
126
+ const result = await postIntegration(auth.apiUrl, auth.token, "anthropic", {
127
+ adminKey: opts.adminKey,
128
+ });
129
+ if (!result)
130
+ return;
131
+ if (!reportConnectResult("anthropic", result))
132
+ return;
133
+ console.log("Anthropic integration connected.");
134
+ console.log("PlanckSpace reads only the usage report endpoint (GET /v1/organizations/usage_report/messages).");
135
+ console.log("The admin key is stored encrypted server-side and never persisted locally.");
136
+ }
137
+ // ─── openai ─────────────────────────────────────────────────────────────────
138
+ export async function runConnectOpenAI(opts) {
139
+ const auth = await requireAuth();
140
+ if (!auth)
141
+ return;
142
+ const result = await postIntegration(auth.apiUrl, auth.token, "openai", {
143
+ apiKey: opts.apiKey,
144
+ });
145
+ if (!result)
146
+ return;
147
+ if (!reportConnectResult("openai", result))
148
+ return;
149
+ console.log("OpenAI integration connected.");
150
+ console.log("PlanckSpace reads only the organization cost endpoint (GET /v1/organization/costs).");
151
+ console.log("The API key is stored encrypted server-side and never persisted locally.");
152
+ }
153
+ // ─── bedrock ────────────────────────────────────────────────────────────────
154
+ function printBedrockInstructions(workspaceId) {
155
+ const externalId = workspaceId ?? "<your workspace ID — run `planck status`>";
156
+ console.log("AWS Bedrock connection — the IAM role must grant read-only access:\n");
157
+ console.log(" Permissions policy (inline on the role):");
158
+ console.log(" ce:GetCostAndUsage — read Bedrock model costs from AWS Cost Explorer");
159
+ console.log(" (no write, no bedrock:InvokeModel, no other service)\n");
160
+ console.log(" Trust policy — let PlanckSpace assume the role:");
161
+ console.log(" Principal: { \"AWS\": \"arn:aws:iam::PLANCKSPACE_ACCOUNT_ID:root\" }");
162
+ console.log(" Action: sts:AssumeRole");
163
+ console.log(` Condition: StringEquals { "sts:ExternalId": "${externalId}" }`);
164
+ console.log("\nThe role is strictly read-only. PlanckSpace never writes to your AWS account.\n");
165
+ }
166
+ export async function runConnectBedrock(opts) {
167
+ const auth = await requireAuth();
168
+ if (!auth)
169
+ return;
170
+ if (!BEDROCK_ARN_RE.test(opts.roleArn)) {
171
+ fail("Invalid role ARN format. Expected: arn:aws:iam::<12-digit-account>:role/<role-name>");
172
+ return;
173
+ }
174
+ printBedrockInstructions(auth.workspaceId);
175
+ // Send the workspace ID as the STS ExternalId so the backend's AssumeRole
176
+ // satisfies the trust policy's condition above.
177
+ const body = { roleArn: opts.roleArn };
178
+ if (auth.workspaceId)
179
+ body.externalId = auth.workspaceId;
180
+ const result = await postIntegration(auth.apiUrl, auth.token, "bedrock", body);
181
+ if (!result)
182
+ return;
183
+ if (!reportConnectResult("bedrock", result)) {
184
+ if (result.status === 422) {
185
+ console.error("Fix the IAM role per the instructions above, then re-run `planck connect bedrock`.");
186
+ }
187
+ return;
188
+ }
189
+ console.log("Bedrock integration connected and validated.");
190
+ }
191
+ //# sourceMappingURL=connect.js.map