@geteigen/cli 0.0.0-stage → 3.0.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 (168) hide show
  1. package/README.md +154 -2
  2. package/dist/agent.d.ts +32 -0
  3. package/dist/agent.js +246 -0
  4. package/dist/auth.d.ts +5 -0
  5. package/dist/auth.js +33 -0
  6. package/dist/cli/src/agent.d.ts +24 -0
  7. package/dist/cli/src/agent.js +260 -0
  8. package/dist/cli/src/agentTools.d.ts +10 -0
  9. package/dist/cli/src/agentTools.js +70 -0
  10. package/dist/cli/src/auth.d.ts +5 -0
  11. package/dist/cli/src/auth.js +33 -0
  12. package/dist/cli/src/config.d.ts +11 -0
  13. package/dist/cli/src/config.js +51 -0
  14. package/dist/cli/src/cursorAuth.d.ts +10 -0
  15. package/dist/cli/src/cursorAuth.js +92 -0
  16. package/dist/cli/src/index.d.ts +2 -0
  17. package/dist/cli/src/index.js +244 -0
  18. package/dist/cli/src/listModels.d.ts +14 -0
  19. package/dist/cli/src/listModels.js +196 -0
  20. package/dist/cli/src/listSessions.d.ts +9 -0
  21. package/dist/cli/src/listSessions.js +37 -0
  22. package/dist/cli/src/machineConfig.d.ts +20 -0
  23. package/dist/cli/src/machineConfig.js +66 -0
  24. package/dist/cli/src/pickFolder.d.ts +5 -0
  25. package/dist/cli/src/pickFolder.js +65 -0
  26. package/dist/cli/src/proc.d.ts +23 -0
  27. package/dist/cli/src/proc.js +77 -0
  28. package/dist/cli/src/service.d.ts +17 -0
  29. package/dist/cli/src/service.js +246 -0
  30. package/dist/cli/src/workRoots.d.ts +30 -0
  31. package/dist/cli/src/workRoots.js +221 -0
  32. package/dist/cli/src/worker.d.ts +8 -0
  33. package/dist/cli/src/worker.js +51 -0
  34. package/dist/cli/src/workerCore.d.ts +47 -0
  35. package/dist/cli/src/workerCore.js +702 -0
  36. package/dist/cli/src/workspace.d.ts +8 -0
  37. package/dist/cli/src/workspace.js +269 -0
  38. package/dist/config.d.ts +11 -0
  39. package/dist/config.js +51 -0
  40. package/dist/cursorAuth.d.ts +10 -0
  41. package/dist/cursorAuth.js +95 -0
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.js +108 -0
  44. package/dist/listModels.d.ts +6 -0
  45. package/dist/listModels.js +62 -0
  46. package/dist/listSessions.d.ts +9 -0
  47. package/dist/listSessions.js +37 -0
  48. package/dist/pickFolder.d.ts +5 -0
  49. package/dist/pickFolder.js +65 -0
  50. package/dist/shared/actionDrafts.d.ts +83 -0
  51. package/dist/shared/actionDrafts.js +169 -0
  52. package/dist/shared/agentArtifacts.d.ts +49 -0
  53. package/dist/shared/agentArtifacts.js +169 -0
  54. package/dist/shared/agentLabel.d.ts +18 -0
  55. package/dist/shared/agentLabel.js +22 -0
  56. package/dist/shared/agentMentions.d.ts +38 -0
  57. package/dist/shared/agentMentions.js +110 -0
  58. package/dist/shared/agentRepos.d.ts +28 -0
  59. package/dist/shared/agentRepos.js +60 -0
  60. package/dist/shared/agentRunStatus.d.ts +12 -0
  61. package/dist/shared/agentRunStatus.js +17 -0
  62. package/dist/shared/agentUsage.d.ts +24 -0
  63. package/dist/shared/agentUsage.js +139 -0
  64. package/dist/shared/approvals.d.ts +44 -0
  65. package/dist/shared/approvals.js +97 -0
  66. package/dist/shared/askEigen.d.ts +190 -0
  67. package/dist/shared/askEigen.js +76 -0
  68. package/dist/shared/attribution.d.ts +25 -0
  69. package/dist/shared/attribution.js +63 -0
  70. package/dist/shared/automations.d.ts +136 -0
  71. package/dist/shared/automations.js +546 -0
  72. package/dist/shared/backends/claudeCode.d.ts +42 -0
  73. package/dist/shared/backends/claudeCode.js +540 -0
  74. package/dist/shared/backends/codex.d.ts +15 -0
  75. package/dist/shared/backends/codex.js +287 -0
  76. package/dist/shared/backends/cursor.d.ts +102 -0
  77. package/dist/shared/backends/cursor.js +1197 -0
  78. package/dist/shared/backends/index.d.ts +18 -0
  79. package/dist/shared/backends/index.js +58 -0
  80. package/dist/shared/backends/opencode.d.ts +40 -0
  81. package/dist/shared/backends/opencode.js +397 -0
  82. package/dist/shared/backends/types.d.ts +171 -0
  83. package/dist/shared/backends/types.js +19 -0
  84. package/dist/shared/backends/usage.d.ts +8 -0
  85. package/dist/shared/backends/usage.js +71 -0
  86. package/dist/shared/billingPlans.d.ts +60 -0
  87. package/dist/shared/billingPlans.js +262 -0
  88. package/dist/shared/brain.d.ts +353 -0
  89. package/dist/shared/brain.js +487 -0
  90. package/dist/shared/clarifyingQuestions.d.ts +13 -0
  91. package/dist/shared/clarifyingQuestions.js +207 -0
  92. package/dist/shared/claudeModels.d.ts +10 -0
  93. package/dist/shared/claudeModels.js +52 -0
  94. package/dist/shared/cloudBackends.d.ts +21 -0
  95. package/dist/shared/cloudBackends.js +29 -0
  96. package/dist/shared/codexModels.d.ts +10 -0
  97. package/dist/shared/codexModels.js +28 -0
  98. package/dist/shared/contextWindows.d.ts +2 -0
  99. package/dist/shared/contextWindows.js +30 -0
  100. package/dist/shared/cursorMcpConfig.d.ts +14 -0
  101. package/dist/shared/cursorMcpConfig.js +43 -0
  102. package/dist/shared/cursorProjectMcp.d.ts +10 -0
  103. package/dist/shared/cursorProjectMcp.js +42 -0
  104. package/dist/shared/docs.d.ts +59 -0
  105. package/dist/shared/docs.js +87 -0
  106. package/dist/shared/events.d.ts +929 -0
  107. package/dist/shared/events.js +59 -0
  108. package/dist/shared/integrations.d.ts +266 -0
  109. package/dist/shared/integrations.js +449 -0
  110. package/dist/shared/issueTranscript.d.ts +64 -0
  111. package/dist/shared/issueTranscript.js +387 -0
  112. package/dist/shared/issues.d.ts +139 -0
  113. package/dist/shared/issues.js +217 -0
  114. package/dist/shared/machines.d.ts +115 -0
  115. package/dist/shared/machines.js +58 -0
  116. package/dist/shared/markdownMedia.d.ts +13 -0
  117. package/dist/shared/markdownMedia.js +41 -0
  118. package/dist/shared/notificationText.d.ts +6 -0
  119. package/dist/shared/notificationText.js +38 -0
  120. package/dist/shared/opencodeModels.d.ts +19 -0
  121. package/dist/shared/opencodeModels.js +62 -0
  122. package/dist/shared/opencodeSession.d.ts +7 -0
  123. package/dist/shared/opencodeSession.js +30 -0
  124. package/dist/shared/orchestration.d.ts +58 -0
  125. package/dist/shared/orchestration.js +12 -0
  126. package/dist/shared/orgs.d.ts +47 -0
  127. package/dist/shared/orgs.js +49 -0
  128. package/dist/shared/personalOrg.d.ts +5 -0
  129. package/dist/shared/personalOrg.js +12 -0
  130. package/dist/shared/plans.d.ts +3 -0
  131. package/dist/shared/plans.js +19 -0
  132. package/dist/shared/providerModels.d.ts +40 -0
  133. package/dist/shared/providerModels.js +503 -0
  134. package/dist/shared/roomAttention.d.ts +17 -0
  135. package/dist/shared/roomAttention.js +36 -0
  136. package/dist/shared/roomContext.d.ts +120 -0
  137. package/dist/shared/roomContext.js +133 -0
  138. package/dist/shared/roomPermissions.d.ts +56 -0
  139. package/dist/shared/roomPermissions.js +133 -0
  140. package/dist/shared/sessionSeats.d.ts +29 -0
  141. package/dist/shared/sessionSeats.js +57 -0
  142. package/dist/shared/slashCommands.d.ts +39 -0
  143. package/dist/shared/slashCommands.js +166 -0
  144. package/dist/shared/space.d.ts +33 -0
  145. package/dist/shared/space.js +54 -0
  146. package/dist/shared/spaceFeed.d.ts +78 -0
  147. package/dist/shared/spaceFeed.js +194 -0
  148. package/dist/shared/stringifyUnknown.d.ts +1 -0
  149. package/dist/shared/stringifyUnknown.js +1 -0
  150. package/dist/shared/swarm.d.ts +320 -0
  151. package/dist/shared/swarm.js +328 -0
  152. package/dist/shared/swarmVisual.d.ts +79 -0
  153. package/dist/shared/swarmVisual.js +212 -0
  154. package/dist/shared/terminalFilter.d.ts +13 -0
  155. package/dist/shared/terminalFilter.js +46 -0
  156. package/dist/shared/toolIntegration.d.ts +56 -0
  157. package/dist/shared/toolIntegration.js +180 -0
  158. package/dist/shared/typing.d.ts +9 -0
  159. package/dist/shared/typing.js +37 -0
  160. package/dist/shared/uploads.d.ts +31 -0
  161. package/dist/shared/uploads.js +65 -0
  162. package/dist/shared/voiceApprovals.d.ts +36 -0
  163. package/dist/shared/voiceApprovals.js +229 -0
  164. package/dist/shared/workspace.d.ts +47 -0
  165. package/dist/shared/workspace.js +40 -0
  166. package/dist/worker.d.ts +1 -0
  167. package/dist/worker.js +266 -0
  168. package/package.json +51 -4
@@ -0,0 +1,110 @@
1
+ /**
2
+ * "@Agent name" / "@Person" mentions in the Space composer.
3
+ *
4
+ * Labels may contain spaces ("Research Agent"), so matching is done against
5
+ * the known agent labels (longest first) rather than a generic @word regex.
6
+ */
7
+ function escapeRegExp(value) {
8
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9
+ }
10
+ function byLongestLabel(agents) {
11
+ return agents
12
+ .filter((a) => a.label.trim())
13
+ .sort((a, b) => b.label.length - a.label.length);
14
+ }
15
+ function mentionPattern(label) {
16
+ // @label not followed by another word character (so "@Agent" ≠ "@Agents").
17
+ return new RegExp(`(^|[^\\w@])@${escapeRegExp(label.trim())}(?![\\w-])`, "gi");
18
+ }
19
+ export function parseAgentMentions(input, agents) {
20
+ const found = [];
21
+ let text = input;
22
+ for (const agent of byLongestLabel(agents)) {
23
+ const re = mentionPattern(agent.label);
24
+ let first = -1;
25
+ text = text.replace(re, (_match, lead, offset) => {
26
+ if (first < 0)
27
+ first = offset;
28
+ // Placeholder keeps offsets stable for shorter labels matched later.
29
+ return `${lead}\u0000${agent.label.trim()}\u0001`;
30
+ });
31
+ if (first >= 0)
32
+ found.push({ id: agent.id, index: first });
33
+ }
34
+ // Leading run of mentions ("@A @B, do X") addresses the agents — drop it.
35
+ text = text.replace(/^(\s*\u0000[^\u0001]*\u0001[\s,:;]*)+/, "");
36
+ // Inline mentions read naturally as plain names.
37
+ text = text.replace(/\u0000([^\u0001]*)\u0001/g, "$1").trim();
38
+ return {
39
+ agentIds: found.sort((a, b) => a.index - b.index).map((f) => f.id),
40
+ text,
41
+ };
42
+ }
43
+ /**
44
+ * Active "@query" at the end of the draft (caret assumed at the end), or
45
+ * null when the user isn't mentioning anyone right now.
46
+ */
47
+ export function activeMentionQuery(draft) {
48
+ const match = /(?:^|\s)@([^@\n]{0,40})$/.exec(draft);
49
+ return match ? match[1] : null;
50
+ }
51
+ /** Agents whose label starts with (or contains) the query. */
52
+ export function matchMentionAgents(agents, query) {
53
+ const q = query.toLowerCase();
54
+ const starts = agents.filter((a) => a.label.toLowerCase().startsWith(q));
55
+ if (q.includes(" "))
56
+ return starts;
57
+ const contains = agents.filter((a) => !starts.includes(a) && a.label.toLowerCase().includes(q));
58
+ return [...starts, ...contains];
59
+ }
60
+ /** Replace the trailing "@query" with a completed "@Label ". */
61
+ export function completeMention(draft, label) {
62
+ return draft.replace(/@([^@\n]{0,40})$/, `@${label} `);
63
+ }
64
+ /** People mentioned in a draft (kept in the text, unlike agent mentions). */
65
+ export function findMentions(text, targets) {
66
+ const found = [];
67
+ let scan = text;
68
+ for (const target of byLongestLabel(targets)) {
69
+ const re = mentionPattern(target.label);
70
+ let first = -1;
71
+ scan = scan.replace(re, (match, lead, offset) => {
72
+ if (first < 0)
73
+ first = offset;
74
+ // Blank out so a shorter label can't match inside a longer one.
75
+ return lead + " ".repeat(match.length - lead.length);
76
+ });
77
+ if (first >= 0)
78
+ found.push({ target, index: first });
79
+ }
80
+ return found.sort((a, b) => a.index - b.index).map((f) => f.target);
81
+ }
82
+ /** Split text into plain runs and "@Label" mentions for rendering chips. */
83
+ export function segmentMentions(text, targets) {
84
+ const hits = [];
85
+ for (const target of byLongestLabel(targets)) {
86
+ const re = mentionPattern(target.label);
87
+ let m;
88
+ while ((m = re.exec(text))) {
89
+ const start = m.index + m[1].length;
90
+ const end = m.index + m[0].length;
91
+ if (hits.some((h) => start < h.end && end > h.start))
92
+ continue;
93
+ hits.push({ start, end, target });
94
+ }
95
+ }
96
+ if (!hits.length)
97
+ return [{ kind: "text", text }];
98
+ hits.sort((a, b) => a.start - b.start);
99
+ const out = [];
100
+ let at = 0;
101
+ for (const h of hits) {
102
+ if (h.start > at)
103
+ out.push({ kind: "text", text: text.slice(at, h.start) });
104
+ out.push({ kind: "mention", text: text.slice(h.start, h.end), target: h.target });
105
+ at = h.end;
106
+ }
107
+ if (at < text.length)
108
+ out.push({ kind: "text", text: text.slice(at) });
109
+ return out;
110
+ }
@@ -0,0 +1,28 @@
1
+ export type AgentRepoSource = "github" | "cursor";
2
+ /** One repository the add-agent picker can show (GitHub account and/or Cursor). */
3
+ export interface AgentRepoChoice {
4
+ url: string;
5
+ fullName: string;
6
+ /** Known from GitHub. Cursor-only rows leave this null. */
7
+ private: boolean | null;
8
+ defaultBranch: string;
9
+ sources: AgentRepoSource[];
10
+ }
11
+ export declare function githubRepoFullName(url: string): string | null;
12
+ /**
13
+ * Merge the workspace GitHub list (includes private repos the token can see)
14
+ * with Cursor Cloud's connected repositories. Same URL appears once.
15
+ */
16
+ export declare function mergeAgentRepos(input: {
17
+ github?: Array<{
18
+ url: string;
19
+ fullName: string;
20
+ private: boolean;
21
+ defaultBranch?: string;
22
+ }>;
23
+ cursor?: Array<{
24
+ url: string;
25
+ }>;
26
+ }): AgentRepoChoice[];
27
+ export declare function filterAgentRepos(repos: AgentRepoChoice[], query: string): AgentRepoChoice[];
28
+ export declare function addAgentHref(roomId: string): string;
@@ -0,0 +1,60 @@
1
+ import { parseGithubHttpsRepo } from "./issues";
2
+ export function githubRepoFullName(url) {
3
+ const parsed = parseGithubHttpsRepo(url);
4
+ if (!parsed)
5
+ return null;
6
+ const m = parsed.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+)$/i);
7
+ return m ? `${m[1]}/${m[2]}` : null;
8
+ }
9
+ function repoKey(url) {
10
+ return (parseGithubHttpsRepo(url) || url.trim()).toLowerCase();
11
+ }
12
+ /**
13
+ * Merge the workspace GitHub list (includes private repos the token can see)
14
+ * with Cursor Cloud's connected repositories. Same URL appears once.
15
+ */
16
+ export function mergeAgentRepos(input) {
17
+ const byKey = new Map();
18
+ for (const row of input.github || []) {
19
+ const url = parseGithubHttpsRepo(row.url) || row.url.trim();
20
+ if (!url)
21
+ continue;
22
+ byKey.set(repoKey(url), {
23
+ url,
24
+ fullName: row.fullName || githubRepoFullName(url) || url,
25
+ private: Boolean(row.private),
26
+ defaultBranch: row.defaultBranch || "main",
27
+ sources: ["github"],
28
+ });
29
+ }
30
+ for (const row of input.cursor || []) {
31
+ const url = parseGithubHttpsRepo(row.url) || row.url.trim();
32
+ if (!url)
33
+ continue;
34
+ const key = repoKey(url);
35
+ const existing = byKey.get(key);
36
+ if (existing) {
37
+ if (!existing.sources.includes("cursor"))
38
+ existing.sources.push("cursor");
39
+ continue;
40
+ }
41
+ byKey.set(key, {
42
+ url,
43
+ fullName: githubRepoFullName(url) || url,
44
+ private: null,
45
+ defaultBranch: "main",
46
+ sources: ["cursor"],
47
+ });
48
+ }
49
+ return [...byKey.values()].sort((a, b) => a.fullName.localeCompare(b.fullName, undefined, { sensitivity: "base" }));
50
+ }
51
+ export function filterAgentRepos(repos, query) {
52
+ const q = query.trim().toLowerCase();
53
+ if (!q)
54
+ return repos;
55
+ return repos.filter((repo) => repo.fullName.toLowerCase().includes(q) ||
56
+ repo.url.toLowerCase().includes(q));
57
+ }
58
+ export function addAgentHref(roomId) {
59
+ return `/room/${encodeURIComponent(roomId)}/agents/new`;
60
+ }
@@ -0,0 +1,12 @@
1
+ import type { AgentRunStatus, AgentStatus } from "./events.js";
2
+ /** Map persisted agent status onto the run states the chat input cares about. */
3
+ export declare function toAgentRunStatus(status?: AgentStatus | AgentRunStatus | string | null): AgentRunStatus;
4
+ /**
5
+ * Busy / error for the agent currently in view.
6
+ * Live socket status wins; otherwise the agent snapshot.
7
+ * Never falls back to another agent's room-level run status.
8
+ */
9
+ export declare function resolveAgentRunStatus(agent: {
10
+ id?: string;
11
+ status?: AgentStatus | string;
12
+ } | null | undefined, statusByAgent?: Record<string, AgentRunStatus>): AgentRunStatus;
@@ -0,0 +1,17 @@
1
+ /** Map persisted agent status onto the run states the chat input cares about. */
2
+ export function toAgentRunStatus(status) {
3
+ if (status === "running" || status === "error")
4
+ return status;
5
+ return "idle";
6
+ }
7
+ /**
8
+ * Busy / error for the agent currently in view.
9
+ * Live socket status wins; otherwise the agent snapshot.
10
+ * Never falls back to another agent's room-level run status.
11
+ */
12
+ export function resolveAgentRunStatus(agent, statusByAgent = {}) {
13
+ const live = agent?.id ? statusByAgent[agent.id] : undefined;
14
+ if (live)
15
+ return toAgentRunStatus(live);
16
+ return toAgentRunStatus(agent?.status);
17
+ }
@@ -0,0 +1,24 @@
1
+ import type { AgentUsageInfo } from "./events.js";
2
+ import type { AgentBackendKind, RunUsage } from "./backends/types.js";
3
+ export type SdkUsageLike = {
4
+ usage?: {
5
+ inputTokens?: number;
6
+ outputTokens?: number;
7
+ cacheReadTokens?: number;
8
+ cacheWriteTokens?: number;
9
+ totalTokens?: number;
10
+ reasoningTokens?: number;
11
+ } | null;
12
+ cost?: {
13
+ rawCostCents?: number;
14
+ chargedCents?: number;
15
+ } | null;
16
+ } | null;
17
+ export declare function agentUsageFromSdk(raw: SdkUsageLike): AgentUsageInfo | null;
18
+ export declare function parseAgentUsageJson(raw: unknown): AgentUsageInfo | undefined;
19
+ /** Add one CLI turn to the persisted usage total. */
20
+ export declare function addRunUsageToAgent(previous: AgentUsageInfo | undefined, run: RunUsage, backend: AgentBackendKind, modelId: string): AgentUsageInfo;
21
+ export declare function formatTokenCount(n: number): string;
22
+ export declare function formatUsageCost(usage: AgentUsageInfo): string | null;
23
+ export declare function formatAgentUsage(usage: AgentUsageInfo): string;
24
+ export declare function formatAgentUsageDetail(usage: AgentUsageInfo): string;
@@ -0,0 +1,139 @@
1
+ import { runUsageCostUsd } from "./backends/usage.js";
2
+ import { contextWindowForModel } from "./contextWindows.js";
3
+ function num(value) {
4
+ const n = typeof value === "number" ? value : Number(value);
5
+ return Number.isFinite(n) ? n : 0;
6
+ }
7
+ export function agentUsageFromSdk(raw) {
8
+ if (!raw?.usage)
9
+ return null;
10
+ const usage = raw.usage;
11
+ const info = {
12
+ inputTokens: num(usage.inputTokens),
13
+ outputTokens: num(usage.outputTokens),
14
+ cacheReadTokens: num(usage.cacheReadTokens),
15
+ cacheWriteTokens: num(usage.cacheWriteTokens),
16
+ totalTokens: num(usage.totalTokens),
17
+ contextTokens: num(usage.inputTokens) +
18
+ num(usage.cacheReadTokens) +
19
+ num(usage.cacheWriteTokens),
20
+ updatedAt: Date.now(),
21
+ };
22
+ if (usage.reasoningTokens != null) {
23
+ info.reasoningTokens = num(usage.reasoningTokens);
24
+ }
25
+ if (raw.cost) {
26
+ info.rawCostCents = num(raw.cost.rawCostCents);
27
+ info.chargedCents = num(raw.cost.chargedCents);
28
+ }
29
+ return info;
30
+ }
31
+ export function parseAgentUsageJson(raw) {
32
+ if (!raw)
33
+ return undefined;
34
+ let parsed = raw;
35
+ if (typeof raw === "string") {
36
+ try {
37
+ parsed = JSON.parse(raw);
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ if (!parsed || typeof parsed !== "object")
44
+ return undefined;
45
+ const row = parsed;
46
+ const totalTokens = num(row.totalTokens);
47
+ const inputTokens = num(row.inputTokens);
48
+ const outputTokens = num(row.outputTokens);
49
+ if (totalTokens <= 0 && inputTokens <= 0 && outputTokens <= 0) {
50
+ return undefined;
51
+ }
52
+ const info = {
53
+ inputTokens,
54
+ outputTokens,
55
+ cacheReadTokens: num(row.cacheReadTokens),
56
+ cacheWriteTokens: num(row.cacheWriteTokens),
57
+ totalTokens: totalTokens || inputTokens + outputTokens,
58
+ updatedAt: num(row.updatedAt) || Date.now(),
59
+ };
60
+ if (row.reasoningTokens != null)
61
+ info.reasoningTokens = num(row.reasoningTokens);
62
+ if (row.rawCostCents != null)
63
+ info.rawCostCents = num(row.rawCostCents);
64
+ if (row.chargedCents != null)
65
+ info.chargedCents = num(row.chargedCents);
66
+ if (row.contextTokens != null)
67
+ info.contextTokens = num(row.contextTokens);
68
+ if (row.contextWindowTokens != null) {
69
+ info.contextWindowTokens = num(row.contextWindowTokens);
70
+ }
71
+ return info;
72
+ }
73
+ /** Add one CLI turn to the persisted usage total. */
74
+ export function addRunUsageToAgent(previous, run, backend, modelId) {
75
+ const input = (previous?.inputTokens ?? 0) + run.inputTokens;
76
+ const output = (previous?.outputTokens ?? 0) + run.outputTokens;
77
+ const cacheRead = (previous?.cacheReadTokens ?? 0) + run.cacheReadTokens;
78
+ const cacheWrite = (previous?.cacheWriteTokens ?? 0) + run.cacheWriteTokens;
79
+ const costCents = Math.round(runUsageCostUsd(backend, run) * 100);
80
+ const info = {
81
+ inputTokens: input,
82
+ outputTokens: output,
83
+ cacheReadTokens: cacheRead,
84
+ cacheWriteTokens: cacheWrite,
85
+ totalTokens: (previous?.totalTokens ?? 0) + run.inputTokens + run.outputTokens,
86
+ contextTokens: run.inputTokens + run.cacheReadTokens + run.cacheWriteTokens,
87
+ updatedAt: Date.now(),
88
+ };
89
+ const window = contextWindowForModel(backend, modelId);
90
+ if (window)
91
+ info.contextWindowTokens = window;
92
+ else if (previous?.contextWindowTokens) {
93
+ info.contextWindowTokens = previous.contextWindowTokens;
94
+ }
95
+ const rawCostCents = (previous?.rawCostCents ?? 0) + costCents;
96
+ const chargedCents = (previous?.chargedCents ?? 0) + costCents;
97
+ if (rawCostCents)
98
+ info.rawCostCents = rawCostCents;
99
+ if (chargedCents)
100
+ info.chargedCents = chargedCents;
101
+ return info;
102
+ }
103
+ export function formatTokenCount(n) {
104
+ const abs = Math.max(0, Math.round(n));
105
+ if (abs < 1000)
106
+ return `${abs}`;
107
+ if (abs < 10_000)
108
+ return `${(abs / 1000).toFixed(1).replace(/\.0$/, "")}k`;
109
+ if (abs < 1_000_000)
110
+ return `${Math.round(abs / 1000)}k`;
111
+ return `${(abs / 1_000_000).toFixed(1).replace(/\.0$/, "")}M`;
112
+ }
113
+ export function formatUsageCost(usage) {
114
+ const cents = Math.max(usage.chargedCents ?? 0, usage.rawCostCents ?? 0);
115
+ if (!cents)
116
+ return null;
117
+ const usd = cents / 100;
118
+ if (usd < 0.01)
119
+ return "<$0.01";
120
+ return `$${usd.toFixed(usd < 10 ? 2 : 2)}`;
121
+ }
122
+ export function formatAgentUsage(usage) {
123
+ const tokens = `${formatTokenCount(usage.totalTokens)} tok`;
124
+ const cost = formatUsageCost(usage);
125
+ return cost ? `${tokens} · ${cost}` : tokens;
126
+ }
127
+ export function formatAgentUsageDetail(usage) {
128
+ const parts = [
129
+ `in ${formatTokenCount(usage.inputTokens)}`,
130
+ `out ${formatTokenCount(usage.outputTokens)}`,
131
+ ];
132
+ if (usage.cacheReadTokens) {
133
+ parts.push(`cache ${formatTokenCount(usage.cacheReadTokens)}`);
134
+ }
135
+ const cost = formatUsageCost(usage);
136
+ if (cost)
137
+ parts.push(cost);
138
+ return parts.join(" · ");
139
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Approval gates for high-blast-radius agent actions.
3
+ * Separate from file locks — these need a human sign-off in the room.
4
+ */
5
+ export type ApprovalMode = "off" | "dangerous" | "all";
6
+ export type AgentMode = "agent" | "plan";
7
+ export type ApprovalStatus = "pending" | "approved" | "denied" | "expired";
8
+ export interface ApprovalRequestInfo {
9
+ id: string;
10
+ roomId: string;
11
+ agentId: string;
12
+ callId: string;
13
+ toolName: string;
14
+ detail: string;
15
+ path?: string;
16
+ status: ApprovalStatus;
17
+ createdAt: number;
18
+ decidedAt?: number;
19
+ decidedByUserId?: string;
20
+ decidedByName?: string;
21
+ }
22
+ export declare function parseApprovalMode(raw: unknown, fallback?: ApprovalMode): ApprovalMode;
23
+ export declare function parseAgentMode(raw: unknown, fallback?: AgentMode): AgentMode;
24
+ export declare function approvalModeLabel(mode: ApprovalMode): string;
25
+ export declare function approvalModeDescription(mode: ApprovalMode): string;
26
+ export declare function agentModeLabel(mode: AgentMode): string;
27
+ export declare function agentModeDescription(mode: AgentMode): string;
28
+ export declare function isShellTool(name: string): boolean;
29
+ export declare function isDeleteTool(name: string): boolean;
30
+ export declare function isSensitivePath(path: string | undefined): boolean;
31
+ export declare function isDangerousShellCommand(detail: string | undefined): boolean;
32
+ /**
33
+ * Whether this tool call should pause for human approval given the room mode.
34
+ * `isEdit` is injected so we don't duplicate EDIT_TOOL_RE across modules.
35
+ */
36
+ export declare function requiresApproval(opts: {
37
+ mode: ApprovalMode;
38
+ toolName: string;
39
+ detail?: string;
40
+ path?: string;
41
+ isEditTool: (name: string) => boolean;
42
+ }): boolean;
43
+ /** Stable key so an approved action can be resumed without re-prompting. */
44
+ export declare function approvalActionKey(toolName: string, detail: string, path?: string): string;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Approval gates for high-blast-radius agent actions.
3
+ * Separate from file locks — these need a human sign-off in the room.
4
+ */
5
+ export function parseApprovalMode(raw, fallback = "off") {
6
+ const v = String(raw ?? "").trim().toLowerCase();
7
+ if (v === "off" || v === "dangerous" || v === "all")
8
+ return v;
9
+ return fallback;
10
+ }
11
+ export function parseAgentMode(raw, fallback = "agent") {
12
+ const v = String(raw ?? "").trim().toLowerCase();
13
+ if (v === "plan" || v === "agent")
14
+ return v;
15
+ if (v === "true" || v === "1")
16
+ return "plan";
17
+ if (v === "false" || v === "0")
18
+ return "agent";
19
+ return fallback;
20
+ }
21
+ export function approvalModeLabel(mode) {
22
+ switch (mode) {
23
+ case "off":
24
+ return "No approvals";
25
+ case "dangerous":
26
+ return "Dangerous actions";
27
+ case "all":
28
+ return "All edits & shell";
29
+ }
30
+ }
31
+ export function approvalModeDescription(mode) {
32
+ switch (mode) {
33
+ case "off":
34
+ return "Agents run tools without a second human sign-off.";
35
+ case "dangerous":
36
+ return "Migrations, deletes, pushes to main, CI/secrets changes pause for approval.";
37
+ case "all":
38
+ return "Every edit and shell command pauses for room approval.";
39
+ }
40
+ }
41
+ export function agentModeLabel(mode) {
42
+ return mode === "plan" ? "Plan" : "Agent";
43
+ }
44
+ export function agentModeDescription(mode) {
45
+ return mode === "plan"
46
+ ? "Read-only planning — the agent explores and proposes without editing."
47
+ : "Full agent mode — the agent can edit files and run commands.";
48
+ }
49
+ /** Shell / terminal tool names across agent backends. */
50
+ const SHELL_TOOL_RE = /^(shell|bash|run_terminal_cmd|runterminalcmd|terminal|execute|command|Bash)$/i;
51
+ /** Explicit delete tools. */
52
+ const DELETE_TOOL_RE = /^(delete|deletefile|delete_file|Remove|remove)$/i;
53
+ /** High-blast-radius shell command patterns. */
54
+ const DANGEROUS_SHELL_RE = /\b(rm\s+(-[a-zA-Z]*r[a-zA-Z]*f|-[a-zA-Z]*f[a-zA-Z]*r)|rm\s+-rf|rmdir\s+-r|sudo\s+|drop\s+(table|database|schema)|truncate\s+table|migrate|prisma\s+migrate|knex\s+migrate|django(-admin)?\s+migrate|rails\s+db:migrate|flyway|liquibase|git\s+push\s+.*\b(--force|-f)\b|git\s+push\s+.*\b(main|master)\b|git\s+branch\s+(-D|-d)\b|git\s+reset\s+--hard|git\s+clean\s+-f|kubectl\s+delete|terraform\s+destroy|helm\s+uninstall|dd\s+if=|mkfs\.|chmod\s+-R\s+777|curl\s+.*\|\s*(ba)?sh)\b/i;
55
+ /** Paths that need a second pair of eyes before edit/delete. */
56
+ const SENSITIVE_PATH_RE = /(^|\/)(\.env($|\.)|\.env\.[^/]+|.*secrets?.*|.*credentials?.*|\.pem$|\.key$|\.p12$|\.pfx$|id_rsa|id_ed25519|\.npmrc|\.netrc|aws\/credentials|\.kube\/config|\.github\/workflows\/|Jenkinsfile|cloudbuild\.ya?ml|\.gitlab-ci\.ya?ml|azure-pipelines\.ya?ml|bitbucket-pipelines\.ya?ml|buildkite\.ya?ml|circleci\/config\.ya?ml|travis\.ya?ml|Dockerfile|docker-compose.*\.ya?ml|terraform\/|.*\.tf$)/i;
57
+ export function isShellTool(name) {
58
+ const n = name.replace(/ToolCall$/i, "");
59
+ return SHELL_TOOL_RE.test(n);
60
+ }
61
+ export function isDeleteTool(name) {
62
+ const n = name.replace(/ToolCall$/i, "");
63
+ return DELETE_TOOL_RE.test(n) || /^delete/i.test(n);
64
+ }
65
+ export function isSensitivePath(path) {
66
+ if (!path?.trim())
67
+ return false;
68
+ return SENSITIVE_PATH_RE.test(path.replace(/\\/g, "/"));
69
+ }
70
+ export function isDangerousShellCommand(detail) {
71
+ if (!detail?.trim())
72
+ return false;
73
+ return DANGEROUS_SHELL_RE.test(detail);
74
+ }
75
+ /**
76
+ * Whether this tool call should pause for human approval given the room mode.
77
+ * `isEdit` is injected so we don't duplicate EDIT_TOOL_RE across modules.
78
+ */
79
+ export function requiresApproval(opts) {
80
+ const { mode, toolName, detail, path, isEditTool } = opts;
81
+ if (mode === "off")
82
+ return false;
83
+ const shell = isShellTool(toolName);
84
+ const del = isDeleteTool(toolName);
85
+ const edit = isEditTool(toolName);
86
+ const sensitive = isSensitivePath(path) || isSensitivePath(detail);
87
+ const dangerousShell = shell && isDangerousShellCommand(detail);
88
+ if (mode === "all") {
89
+ return shell || edit || del;
90
+ }
91
+ // dangerous
92
+ return dangerousShell || del || (edit && sensitive) || (shell && sensitive);
93
+ }
94
+ /** Stable key so an approved action can be resumed without re-prompting. */
95
+ export function approvalActionKey(toolName, detail, path) {
96
+ return `${toolName.trim().toLowerCase()}::${(path || "").trim()}::${detail.trim()}`.slice(0, 500);
97
+ }