@onepointfour-packs/flaredeck-agent 0.1.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 (148) hide show
  1. package/LICENSE.md +14 -0
  2. package/README.md +102 -0
  3. package/dist/agent.d.ts +104 -0
  4. package/dist/agent.js +329 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/approval.d.ts +72 -0
  7. package/dist/approval.js +161 -0
  8. package/dist/approval.js.map +1 -0
  9. package/dist/approvals.d.ts +2 -0
  10. package/dist/approvals.js +148 -0
  11. package/dist/approvals.js.map +1 -0
  12. package/dist/auth.d.ts +79 -0
  13. package/dist/auth.js +181 -0
  14. package/dist/auth.js.map +1 -0
  15. package/dist/automode.d.ts +33 -0
  16. package/dist/automode.js +69 -0
  17. package/dist/automode.js.map +1 -0
  18. package/dist/branches.d.ts +25 -0
  19. package/dist/branches.js +81 -0
  20. package/dist/branches.js.map +1 -0
  21. package/dist/cli.d.ts +2 -0
  22. package/dist/cli.js +12 -0
  23. package/dist/cli.js.map +1 -0
  24. package/dist/codesearch.d.ts +27 -0
  25. package/dist/codesearch.js +111 -0
  26. package/dist/codesearch.js.map +1 -0
  27. package/dist/concurrency.d.ts +14 -0
  28. package/dist/concurrency.js +52 -0
  29. package/dist/concurrency.js.map +1 -0
  30. package/dist/config.d.ts +106 -0
  31. package/dist/config.js +119 -0
  32. package/dist/config.js.map +1 -0
  33. package/dist/engine-antigravity.d.ts +23 -0
  34. package/dist/engine-antigravity.js +357 -0
  35. package/dist/engine-antigravity.js.map +1 -0
  36. package/dist/engine-claude.d.ts +18 -0
  37. package/dist/engine-claude.js +64 -0
  38. package/dist/engine-claude.js.map +1 -0
  39. package/dist/engine.d.ts +122 -0
  40. package/dist/engine.js +36 -0
  41. package/dist/engine.js.map +1 -0
  42. package/dist/env.d.ts +1 -0
  43. package/dist/env.js +10 -0
  44. package/dist/env.js.map +1 -0
  45. package/dist/environment.d.ts +39 -0
  46. package/dist/environment.js +100 -0
  47. package/dist/environment.js.map +1 -0
  48. package/dist/flaredeck-check.d.ts +21 -0
  49. package/dist/flaredeck-check.js +12 -0
  50. package/dist/flaredeck-check.js.map +1 -0
  51. package/dist/flaredeck-proxy.d.ts +60 -0
  52. package/dist/flaredeck-proxy.js +186 -0
  53. package/dist/flaredeck-proxy.js.map +1 -0
  54. package/dist/flaredeck.d.ts +132 -0
  55. package/dist/flaredeck.js +106 -0
  56. package/dist/flaredeck.js.map +1 -0
  57. package/dist/gemini.d.ts +21 -0
  58. package/dist/gemini.js +59 -0
  59. package/dist/gemini.js.map +1 -0
  60. package/dist/gitguard.d.ts +2 -0
  61. package/dist/gitguard.js +90 -0
  62. package/dist/gitguard.js.map +1 -0
  63. package/dist/github.d.ts +38 -0
  64. package/dist/github.js +153 -0
  65. package/dist/github.js.map +1 -0
  66. package/dist/githubauth.d.ts +21 -0
  67. package/dist/githubauth.js +90 -0
  68. package/dist/githubauth.js.map +1 -0
  69. package/dist/intake.d.ts +124 -0
  70. package/dist/intake.js +383 -0
  71. package/dist/intake.js.map +1 -0
  72. package/dist/jam.d.ts +45 -0
  73. package/dist/jam.js +385 -0
  74. package/dist/jam.js.map +1 -0
  75. package/dist/jev.d.ts +31 -0
  76. package/dist/jev.js +75 -0
  77. package/dist/jev.js.map +1 -0
  78. package/dist/jevguard.d.ts +28 -0
  79. package/dist/jevguard.js +213 -0
  80. package/dist/jevguard.js.map +1 -0
  81. package/dist/mailbox.d.ts +22 -0
  82. package/dist/mailbox.js +51 -0
  83. package/dist/mailbox.js.map +1 -0
  84. package/dist/main.d.ts +2 -0
  85. package/dist/main.js +764 -0
  86. package/dist/main.js.map +1 -0
  87. package/dist/markdown.d.ts +5 -0
  88. package/dist/markdown.js +81 -0
  89. package/dist/markdown.js.map +1 -0
  90. package/dist/oauth.d.ts +30 -0
  91. package/dist/oauth.js +216 -0
  92. package/dist/oauth.js.map +1 -0
  93. package/dist/permissions.d.ts +43 -0
  94. package/dist/permissions.js +57 -0
  95. package/dist/permissions.js.map +1 -0
  96. package/dist/preflight.d.ts +71 -0
  97. package/dist/preflight.js +116 -0
  98. package/dist/preflight.js.map +1 -0
  99. package/dist/providers.d.ts +26 -0
  100. package/dist/providers.js +27 -0
  101. package/dist/providers.js.map +1 -0
  102. package/dist/repopicker.d.ts +3 -0
  103. package/dist/repopicker.js +111 -0
  104. package/dist/repopicker.js.map +1 -0
  105. package/dist/runs.d.ts +59 -0
  106. package/dist/runs.js +211 -0
  107. package/dist/runs.js.map +1 -0
  108. package/dist/sandbox.d.ts +13 -0
  109. package/dist/sandbox.js +110 -0
  110. package/dist/sandbox.js.map +1 -0
  111. package/dist/schedules.d.ts +11 -0
  112. package/dist/schedules.js +85 -0
  113. package/dist/schedules.js.map +1 -0
  114. package/dist/secretguard.d.ts +4 -0
  115. package/dist/secretguard.js +115 -0
  116. package/dist/secretguard.js.map +1 -0
  117. package/dist/secrets.d.ts +27 -0
  118. package/dist/secrets.js +51 -0
  119. package/dist/secrets.js.map +1 -0
  120. package/dist/serve.d.ts +17 -0
  121. package/dist/serve.js +1206 -0
  122. package/dist/serve.js.map +1 -0
  123. package/dist/status.d.ts +73 -0
  124. package/dist/status.js +98 -0
  125. package/dist/status.js.map +1 -0
  126. package/dist/stream.d.ts +91 -0
  127. package/dist/stream.js +103 -0
  128. package/dist/stream.js.map +1 -0
  129. package/dist/streamtoken.d.ts +2 -0
  130. package/dist/streamtoken.js +30 -0
  131. package/dist/streamtoken.js.map +1 -0
  132. package/dist/team.d.ts +61 -0
  133. package/dist/team.js +136 -0
  134. package/dist/team.js.map +1 -0
  135. package/dist/ui.d.ts +110 -0
  136. package/dist/ui.js +348 -0
  137. package/dist/ui.js.map +1 -0
  138. package/dist/updater.d.ts +15 -0
  139. package/dist/updater.js +178 -0
  140. package/dist/updater.js.map +1 -0
  141. package/dist/workspace.d.ts +115 -0
  142. package/dist/workspace.js +247 -0
  143. package/dist/workspace.js.map +1 -0
  144. package/dist/worktree.d.ts +42 -0
  145. package/dist/worktree.js +140 -0
  146. package/dist/worktree.js.map +1 -0
  147. package/package.json +52 -0
  148. package/python/sidecar.py +178 -0
package/dist/main.js ADDED
@@ -0,0 +1,764 @@
1
+ #!/usr/bin/env node
2
+ import './env.js';
3
+ import { existsSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { buildOptions, runTask } from './agent.js';
6
+ import { claudeAuth, claudeInstalled, setupToken } from './auth.js';
7
+ import { configOrDefaults, DEFAULTS, forgetConfig, loadConfig, loadMe, saveConfig, saveMe } from './config.js';
8
+ import { startApprovals } from './approvals.js';
9
+ import { flaredeckDecider } from './approval.js';
10
+ import { preflightFlaredeck } from './flaredeck-check.js';
11
+ import { flaredeckProxy } from './flaredeck-proxy.js';
12
+ import { TEAM_TOOL_NAMES } from './team.js';
13
+ import { SCHEDULE_TOOL_NAMES } from './schedules.js';
14
+ import { GIT_TOOL_NAMES } from './github.js';
15
+ import { RUN_TOOL_NAMES } from './runs.js';
16
+ import { askBaseBranch } from './branches.js';
17
+ import { askRepos, checkouts } from './workspace.js';
18
+ import { authorize, freshAccessToken } from './oauth.js';
19
+ import { authorizeGitHub } from './githubauth.js';
20
+ import { loadBaseBranch } from './preflight.js';
21
+ import { serve } from './serve.js';
22
+ import { Flaredeck } from './flaredeck.js';
23
+ import { briefText, intake, intakeText } from './intake.js';
24
+ import { createInterface } from 'node:readline/promises';
25
+ import { loadProviders, saveProviders } from './providers.js';
26
+ import { removeSecret, secretNames, secretsPath, setSecret } from './secrets.js';
27
+ import { loadJam, loginJam } from './jam.js';
28
+ import { denyingDecider, readToolNames, terminalDecider } from './permissions.js';
29
+ const say = (s = '') => process.stdout.write(`${s}\n`);
30
+ // Reads are pre-approved deliberately, so the SDK's "canUseTool will not be invoked" warning is
31
+ // expected for exactly those tools. Swallow it when it names only read tools; let anything else
32
+ // through, because then it would mean a write had been auto-approved and the broker was off.
33
+ //
34
+ // Node prints warnings through its OWN listener, so adding one does not replace it — without
35
+ // this the daemon repeats sixteen tool names on every single turn, which buries the one line
36
+ // that matters. Dropping the default and re-emitting what we do care about is the only way to
37
+ // keep the log readable.
38
+ const defaultWarn = process.listeners('warning');
39
+ process.removeAllListeners('warning');
40
+ process.on('warning', (w) => {
41
+ if (w.code !== 'CLAUDE_SDK_CAN_USE_TOOL_SHADOWED') {
42
+ // Anything else still deserves Node's normal treatment.
43
+ for (const l of defaultWarn)
44
+ l(w);
45
+ return;
46
+ }
47
+ const shadowed = w.message.match(/mcp__[\w-]+__[\w-]+/g) ?? [];
48
+ // Pre-approved on purpose: FlareDeck's reads; the team view, which is a read; and the git
49
+ // tools, which are gated in their own code (the card's branch only, merge behind its own
50
+ // decider) rather than by the broker. Anything else auto-approved is worth a line.
51
+ const expected = new Set([
52
+ ...readToolNames(configOrDefaults().mcpServerName),
53
+ ...TEAM_TOOL_NAMES,
54
+ ...GIT_TOOL_NAMES,
55
+ // Asking for a production run is pre-approved; the run itself waits for a person's Approve.
56
+ ...RUN_TOOL_NAMES,
57
+ ...SCHEDULE_TOOL_NAMES,
58
+ ]);
59
+ const unexpected = shadowed.filter((t) => !expected.has(t));
60
+ if (unexpected.length) {
61
+ process.stderr.write(`⚠ these writes bypass the permission broker: ${unexpected.join(', ')}\n`);
62
+ }
63
+ });
64
+ const die = (s) => {
65
+ process.stderr.write(`${s}\n`);
66
+ process.exit(1);
67
+ };
68
+ /** Who the HARNESS is, read straight out of its own token. No network, no model turn. */
69
+ function identityOf(config) {
70
+ const tok = config.oauth?.accessToken;
71
+ if (!tok)
72
+ return null;
73
+ try {
74
+ const part = tok.split('.')[1] ?? '';
75
+ const claims = JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
76
+ return `${claims.name || claims.email || claims.sub} (${claims.tenantId})`;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /** The MCP server's origin — the OAuth issuer too, since it hosts the provider. */
83
+ const mcpOrigin = (config) => new URL(config.mcpUrl).origin;
84
+ const flag = (argv, name) => {
85
+ const i = argv.indexOf(name);
86
+ return i >= 0 ? argv[i + 1] : undefined;
87
+ };
88
+ /**
89
+ * Who answers a permission request.
90
+ *
91
+ * With a card to ask on, it goes to FlareDeck and anyone on the board can answer. `--ask` prompts
92
+ * at the terminal, for trying things by hand. With neither, it refuses — safe, and honest about
93
+ * there being nobody to ask.
94
+ */
95
+ function deciderFor(argv, config) {
96
+ if (argv.includes('--ask'))
97
+ return terminalDecider;
98
+ const card = flag(argv, '--card');
99
+ const tenantId = config.oauth ? tenantOf(config) : null;
100
+ if (card && tenantId && config.oauth) {
101
+ return flaredeckDecider(new Flaredeck(tenantId, () => harnessToken(config)), card);
102
+ }
103
+ return denyingDecider;
104
+ }
105
+ /** The agent's own tenant, read from its token — nothing to configure. */
106
+ function tenantOf(config) {
107
+ try {
108
+ const part = config.oauth.accessToken.split('.')[1] ?? '';
109
+ return JSON.parse(Buffer.from(part, 'base64url').toString('utf8')).tenantId ?? null;
110
+ }
111
+ catch {
112
+ return null;
113
+ }
114
+ }
115
+ const harnessToken = (config) => freshAccessToken(mcpOrigin(config), config.oauth, (next) => {
116
+ config.oauth = next;
117
+ saveConfig(config);
118
+ });
119
+ async function cmdLogin(argv) {
120
+ if (!(await claudeInstalled())) {
121
+ die('The Claude CLI is not on PATH. Install it first: npm i -g @anthropic-ai/claude-code');
122
+ }
123
+ const repo = flag(argv, '--repo');
124
+ const tenantId = flag(argv, '--tenant');
125
+ const apiKey = flag(argv, '--api-key');
126
+ const config = {
127
+ ...configOrDefaults(),
128
+ ...(repo ? { repo: resolve(repo) } : {}),
129
+ ...(tenantId ? { tenantId } : {}),
130
+ ...(apiKey ? { apiKey } : {}),
131
+ };
132
+ // 1. Who pays for the model.
133
+ const auth = await claudeAuth();
134
+ say(`Claude: ${auth.kind} — ${auth.detail}`);
135
+ if (auth.kind === 'none') {
136
+ say('\nNo Claude credentials. Setting up a long-lived subscription token…');
137
+ await setupToken();
138
+ // `setup-token` PRINTS the token for you to keep; it does not hand it back to us, and this
139
+ // harness does not scrape it off the terminal. So ask again rather than assume: if the CLI
140
+ // still does not know who it is, the token only exists in the scrollback, and saying so now
141
+ // beats starting an agent that cannot reach the model.
142
+ const after = await claudeAuth();
143
+ say(`Claude: ${after.kind} — ${after.detail}`);
144
+ if (after.kind === 'none') {
145
+ say('\nThe token was minted but this machine still cannot use it. Put it in the');
146
+ say('environment the agent starts in, then run this again:');
147
+ say('\n export CLAUDE_CODE_OAUTH_TOKEN=<the sk-ant-oat01-… token printed above>\n');
148
+ die('Claude credentials are not usable yet.');
149
+ }
150
+ }
151
+ // Signed in, but every turn is billed per token. An agent that works unattended for hours is
152
+ // the worst possible thing to leave on metered billing, so this is said out loud at setup
153
+ // rather than discovered on an invoice.
154
+ if (auth.kind === 'metered') {
155
+ say(' · This is metered API billing, not a Claude subscription. An agent that runs');
156
+ say(' unattended will run up a bill — sign in with `claude auth login` on a plan instead.');
157
+ }
158
+ if (process.env['ANTHROPIC_API_KEY']) {
159
+ say(' · ANTHROPIC_API_KEY is set on this box; it is stripped before Claude runs, so this');
160
+ say(' agent stays on the subscription rather than metered billing.');
161
+ }
162
+ // 2. One FlareDeck identity for this machine.
163
+ //
164
+ // This used to be two: the Claude CLI held one for the model's tools, the harness held another
165
+ // for listening. They could drift, and did — the tools acted as one account while the harness
166
+ // listened as another, which looks like it is working. The harness's token is now handed to
167
+ // the MCP server directly, so there is one account and nothing to keep in step.
168
+ // Wrapped at 78 columns by hand, because a terminal over SSH is often 80 wide and a sentence
169
+ // broken mid-word is harder to read than no sentence at all.
170
+ const wrap = (text, width = 78) => {
171
+ const lines = [];
172
+ let line = '';
173
+ for (const word of text.split(' ')) {
174
+ if ((line + ' ' + word).trim().length > width) {
175
+ lines.push(line);
176
+ line = word;
177
+ }
178
+ else
179
+ line = (line + ' ' + word).trim();
180
+ }
181
+ if (line)
182
+ lines.push(line);
183
+ return lines;
184
+ };
185
+ const step = (n, title, why) => {
186
+ say(`\n── ${n} of 3 · ${title} ${'─'.repeat(Math.max(0, 62 - title.length))}`);
187
+ wrap(why).forEach(say);
188
+ say('');
189
+ };
190
+ if (!config.oauth) {
191
+ step(1, 'FlareDeck', 'Which agent this machine acts as. Everything it does on the board is done as that account.');
192
+ config.oauth = await authorize(mcpOrigin(config), say);
193
+ say('✓ FlareDeck authorised');
194
+ }
195
+ else {
196
+ say('\n✓ FlareDeck: already authorised on this machine');
197
+ }
198
+ // 4. GitHub, held by the harness. The agent has publishing tools and no `gh`, so from here
199
+ // the credential is out of its reach rather than merely off-limits.
200
+ // A token handed in directly replaces the OAuth login: for an organisation that restricts
201
+ // OAuth apps and has no owner at hand to approve one, a member's classic personal access
202
+ // token is not subject to that restriction. Stored the same way; the agent still never sees it.
203
+ const givenToken = flag(argv, '--github-token');
204
+ if (givenToken) {
205
+ config.github = { accessToken: givenToken };
206
+ step(2, 'GitHub', 'Using the token you passed (classic PAT, no expiry handling).');
207
+ say('✓ GitHub token stored — the OAuth login is not used while this is set');
208
+ }
209
+ // --github: redo the GitHub step even though one is stored — for a refresh token that has
210
+ // lapsed or been revoked, which otherwise only shows up as a failed push hours later.
211
+ if (argv.includes('--github') && !givenToken)
212
+ delete config.github;
213
+ if (!config.github) {
214
+ step(2, 'GitHub', 'So this machine can push branches and open pull requests. The agent never sees this token.');
215
+ try {
216
+ config.github = await authorizeGitHub(say);
217
+ say('✓ GitHub authorised');
218
+ }
219
+ catch (e) {
220
+ say(`✗ GitHub: ${String(e instanceof Error ? e.message : e)}`);
221
+ say(' Run login again later to finish this; everything else is saved.');
222
+ }
223
+ }
224
+ else {
225
+ say('✓ GitHub: already authorised on this machine');
226
+ }
227
+ // 5. Which branch this work starts from. Asked once, from the repo's real branches — getting
228
+ // it wrong is quiet: every card cut from the wrong history, every PR at the wrong target.
229
+ const declared = loadBaseBranch(config.repo);
230
+ const hasGit = existsSync(join(config.repo, '.git'));
231
+ // A folder holding several repos is not "no checkout" — it is this agent's whole scope. The
232
+ // directory is the declaration (see workspace.ts), so there is nothing to ask and nothing to
233
+ // save: each card names its repository, and each repo answers for its own base branch.
234
+ const found = hasGit ? [] : await checkouts(config.repo);
235
+ if (found.length > 1) {
236
+ step(3, 'Repositories', `This folder holds ${found.length} repositories. Tick the ones this agent may work in — each card's Component says which one a card is for, and each repo answers for its own base branch, so there is no branch to choose here.`);
237
+ const picked = await askRepos(config.repo, say, config.repos);
238
+ if (picked) {
239
+ if (picked.length)
240
+ config.repos = picked;
241
+ else
242
+ delete config.repos;
243
+ }
244
+ }
245
+ else if (found.length === 1) {
246
+ step(3, 'Repositories', `One repository in this folder: ${found[0].name}.`);
247
+ }
248
+ else if (!hasGit) {
249
+ step(3, 'Branch', 'No git repository here and none in this folder, so this agent will have no checkout — right for a lead, wrong for a worker.');
250
+ }
251
+ else if (declared) {
252
+ step(3, 'Branch', `This repo declares its base branch (${declared}), so there is nothing to choose.`);
253
+ }
254
+ else if (!config.baseBranch) {
255
+ step(3, 'Branch', 'Where new work is cut from and merged back into. Cards in a production-fix environment use the board\'s fix branch instead.');
256
+ const picked = await askBaseBranch(config.repo, say);
257
+ if (picked)
258
+ config.baseBranch = picked;
259
+ }
260
+ else {
261
+ step(3, 'Branch', `Already chosen on this machine: ${config.baseBranch}.`);
262
+ }
263
+ /**
264
+ * The key behind the command guard. Asked here rather than left to `providers`, because the
265
+ * guard is not optional any more: without a key every risky command interrupts a person, and
266
+ * someone who has just set this machine up is exactly who should be told that now rather than
267
+ * discovering it the first time an agent wants to run a migration at 2am.
268
+ *
269
+ * Machine-wide, not saved in this agent's config — a key is a billing relationship, and a
270
+ * config file copied to another box should carry no third-party credential.
271
+ */
272
+ const providers = loadProviders();
273
+ if (!providers.openrouter?.apiKey) {
274
+ step(4, 'Command guard', 'An OpenRouter key lets Jev judge risky shell commands — it refuses the irreversible ones and lets the rest run, for a fraction of a cent each. WITHOUT one the guard still runs, but every risky command stops and asks you in person. Get one at https://openrouter.ai/keys. Enter to skip.');
275
+ if (process.stdin.isTTY) {
276
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
277
+ try {
278
+ const entered = (await rl.question('OpenRouter key (sk-or-…), or Enter to skip: ')).trim();
279
+ if (entered) {
280
+ saveProviders({ ...providers, openrouter: { apiKey: entered } });
281
+ say('✓ Saved — the guard will judge commands instead of interrupting you');
282
+ }
283
+ else {
284
+ say(' Skipped. Risky commands will ask you each time; add one later with: flaredeck-agent providers --openrouter <key>');
285
+ }
286
+ }
287
+ finally {
288
+ rl.close();
289
+ }
290
+ }
291
+ }
292
+ else {
293
+ step(4, 'Command guard', 'On, with an OpenRouter key already set on this machine.');
294
+ }
295
+ const path = saveConfig(config);
296
+ const who = identityOf(config) ?? 'unknown';
297
+ say('\n── Done ' + '─'.repeat(63));
298
+ say(` Agent ${who}`);
299
+ const inScope = hasGit ? [] : await checkouts(config.repo, config.repos);
300
+ say(` Repo ${config.repo}${hasGit ? '' : inScope.length ? ` (${inScope.length} of ${found.length} repositories)` : ' (no checkout)'}`);
301
+ if (inScope.length)
302
+ say(` Works in ${inScope.map((c) => c.name).join(', ')}`);
303
+ say(` Branch ${declared ?? config.baseBranch ?? (hasGit ? 'not set' : inScope.length ? "each repo's own" : 'n/a')}`);
304
+ say(` Guard ${loadProviders().openrouter?.apiKey ? 'Jev judges risky commands' : 'on, but every risky command will ask you (no OpenRouter key)'}`);
305
+ say(` Approvals via FlareDeck, as ${who}`);
306
+ say(` Saved ${path}`);
307
+ say('');
308
+ say('Start it with: flaredeck-agent Check it with: flaredeck-agent whoami');
309
+ }
310
+ /** What a token says about who holds it. */
311
+ function claimsOf(accessToken) {
312
+ try {
313
+ const part = accessToken.split('.')[1] ?? '';
314
+ return JSON.parse(Buffer.from(part, 'base64url').toString('utf8'));
315
+ }
316
+ catch {
317
+ return {};
318
+ }
319
+ }
320
+ /**
321
+ * A person signing in, as opposed to a machine being given an agent. The same OAuth flow —
322
+ * sign in as yourself and do NOT connect as an agent — stored apart from every repo's agent
323
+ * config, because it is the account that answers for them.
324
+ */
325
+ async function cmdLoginMe() {
326
+ const config = configOrDefaults();
327
+ say('Sign in as YOURSELF — not as an agent. This login answers agents\' requests; it never works cards.\n');
328
+ const tokens = await authorize(mcpOrigin(config), say);
329
+ const c = claimsOf(tokens.accessToken);
330
+ const path = saveMe(tokens);
331
+ say(`\nSigned in as ${c.name || c.email || 'you'} (${c.tenantId ?? 'tenant unknown'}). Kept at ${path}.`);
332
+ say('Run `flaredeck-agent approvals` to see what agents are waiting on.');
333
+ }
334
+ /** The person's client from `login --me`, or null when nobody has signed in here. */
335
+ /**
336
+ * Which workspace the person answers in. Their token names their PRIMARY workspace, which may
337
+ * not be the one the agents on this machine work in — so the agent's own tenant wins when there
338
+ * is one, and the server accepts the person's token for any workspace they belong to.
339
+ */
340
+ function meTenant(config, me) {
341
+ return (config.oauth ? tenantOf(config) : null) ?? claimsOf(me.accessToken).tenantId ?? null;
342
+ }
343
+ function meClient(config) {
344
+ const stored = loadMe();
345
+ if (!stored)
346
+ return null;
347
+ let me = stored;
348
+ const tenantId = meTenant(config, me);
349
+ if (!tenantId)
350
+ return null;
351
+ return new Flaredeck(tenantId, () => freshAccessToken(mcpOrigin(config), me, (next) => {
352
+ me = next;
353
+ saveMe(next);
354
+ }));
355
+ }
356
+ async function cmdApprovals(argv) {
357
+ const stored = loadMe();
358
+ if (!stored) {
359
+ die('Answering needs a person\'s login, and there is none on this machine yet. Run: flaredeck-agent login --me');
360
+ return;
361
+ }
362
+ let me = stored;
363
+ const c = claimsOf(me.accessToken);
364
+ // The workspace: --tenant, else the agent set up in this directory, else the login's own.
365
+ const config = configOrDefaults();
366
+ const tenantId = flag(argv, '--tenant') ?? meTenant(config, me);
367
+ if (!tenantId) {
368
+ die('That login has no workspace on it. Run from an agent\'s repo, or pass --tenant <id>.');
369
+ return;
370
+ }
371
+ const deck = new Flaredeck(tenantId, () => freshAccessToken(mcpOrigin(config), me, (next) => {
372
+ me = next;
373
+ saveMe(next);
374
+ }));
375
+ await startApprovals(deck, `${c.name || c.email || 'you'} in ${tenantId}`);
376
+ }
377
+ /** Jam, for intake only: the harness reads recordings; the agent never gets a Jam tool. */
378
+ async function cmdLoginJam() {
379
+ say('Sign in to Jam in the workspace whose recordings end up on your cards.');
380
+ say('The harness reads them during intake — transcript, console errors, failed requests — and hands the agent text.\n');
381
+ const path = await loginJam(say);
382
+ say(`\nJam connected. Kept at ${path}. Agents pick it up on their next card; no restart needed.`);
383
+ say('Try it: flaredeck-agent intake <a card with a jam.dev link>');
384
+ }
385
+ /**
386
+ * Keys for the optional classifiers, once per machine. Given as flags so nothing is typed into a
387
+ * prompt that might be echoed; absent flags leave what is stored, and `--clear` forgets all.
388
+ */
389
+ async function cmdProviders(argv) {
390
+ const current = loadProviders();
391
+ if (argv.includes('--clear')) {
392
+ saveProviders({});
393
+ say('Forgot all provider keys on this machine.');
394
+ return;
395
+ }
396
+ const openrouter = flag(argv, '--openrouter');
397
+ const gemini = flag(argv, '--gemini');
398
+ const geminiModel = flag(argv, '--gemini-model');
399
+ const brief = flag(argv, '--brief');
400
+ if (brief && brief !== 'haiku' && brief !== 'gemini') {
401
+ die('--brief must be haiku or gemini');
402
+ return;
403
+ }
404
+ const next = {
405
+ ...current,
406
+ ...(openrouter ? { openrouter: { apiKey: openrouter } } : {}),
407
+ ...(gemini || geminiModel
408
+ ? { gemini: { apiKey: gemini ?? current.gemini?.apiKey ?? '', ...((geminiModel ?? current.gemini?.model) ? { model: geminiModel ?? current.gemini?.model } : {}) } }
409
+ : {}),
410
+ ...(brief ? { brief: brief } : {}),
411
+ };
412
+ if (next.gemini && !next.gemini.apiKey)
413
+ delete next.gemini;
414
+ if (next.brief === 'gemini' && !next.gemini) {
415
+ die('--brief gemini needs a Gemini key: add --gemini <key> (from aistudio.google.com/apikey)');
416
+ return;
417
+ }
418
+ const path = saveProviders(next);
419
+ say(`OpenRouter (Jev): ${next.openrouter ? `set (…${next.openrouter.apiKey.slice(-4)})` : 'not set'}`);
420
+ say(`Gemini: ${next.gemini ? `set (…${next.gemini.apiKey.slice(-4)}) · ${next.gemini.model ?? 'gemini-3.5-flash-lite'}` : 'not set'}`);
421
+ say(`Brief written by: ${next.brief ?? 'haiku'}${next.brief ? '' : ' (default; --brief gemini to switch)'}`);
422
+ say(`Stored in ${path} (0600). Agents pick this up on their next card; no restart needed.`);
423
+ if (!openrouter && !gemini)
424
+ say('\nSet one with: flaredeck-agent providers --openrouter sk-or-… (an OpenRouter key serves Jev)');
425
+ }
426
+ /** The intake step on its own, printed: what the agent would be handed and what it would run on. */
427
+ async function cmdIntake(argv) {
428
+ const card = argv.find((a) => !a.startsWith('--'));
429
+ if (!card) {
430
+ die('Which card? flaredeck-agent intake KD-1234');
431
+ return;
432
+ }
433
+ const config = loadConfig() ?? die('Not set up yet in this directory — run: flaredeck-agent login');
434
+ const tenantId = tenantOf(config);
435
+ if (!tenantId || !config.oauth) {
436
+ die('This agent has no FlareDeck login yet — run: flaredeck-agent login');
437
+ return;
438
+ }
439
+ const deck = new Flaredeck(tenantId, () => harnessToken(config));
440
+ const providers = loadProviders();
441
+ say(`Jev: ${providers.openrouter ? 'on' : 'off (flaredeck-agent providers --openrouter <key>)'} · Gemini: ${providers.gemini ? `on (${providers.gemini.model ?? 'gemini-3.5-flash-lite'})` : 'off (flaredeck-agent providers --gemini <key>)'} · brief by ${providers.brief ?? 'haiku'} · Jam: ${loadJam() ? 'on' : 'off (flaredeck-agent login --jam)'}\n`);
442
+ const out = await intake(deck, card, (t) => say(`· ${t}`), true, config.repo);
443
+ if (!out) {
444
+ die('Intake produced nothing — see above.');
445
+ return;
446
+ }
447
+ say(`· ${out.summary}`);
448
+ if (out.blocked) {
449
+ say(`\nBLOCKED — the agent would not start this card. It would flag needs-human with:\n Cannot start: ${out.blocked}`);
450
+ return;
451
+ }
452
+ say(`· digest ≈ ${Math.round(out.digest.length / 4)} tokens`);
453
+ if (out.jev) {
454
+ say('\nJev:');
455
+ say(` complexity ${out.jev.complexity} (score ${out.jev.score.toFixed(2)}, confidence ${Math.round(out.jev.confidence * 100)}%)`);
456
+ say(` prod data ${Math.round(out.jev.production_data * 100)}%`);
457
+ say(` settled ${Math.round(out.jev.settled * 100)}% (already done or waiting on a person)`);
458
+ say(` kind ${out.jev.kind} (${Math.round(out.jev.kind_confidence * 100)}%)`);
459
+ say(` cost $${out.jev.cost_usd.toFixed(6)} for ${out.jev.input_tokens} input tokens · ${out.jev.model}`);
460
+ }
461
+ say(`\n[what the agent is handed — brief by ${out.briefBy ?? 'nobody'}]` + intakeText(out));
462
+ if (out.other?.brief && out.other.by !== out.briefBy)
463
+ say(`\n[for comparison — brief by ${out.other.by}, ${out.other.secs}s]` + briefText(out.other.brief, out.digest.length));
464
+ say(`\nWould run on: ${out.model ?? 'the SDK default'} (chosen by ${out.routedBy})`);
465
+ }
466
+ async function cmdRun(prompt, argv) {
467
+ const config = loadConfig() ?? die('Not set up yet — run: flaredeck-agent login');
468
+ const card = flag(argv, '--card');
469
+ if (card && !(config.tenantId && config.apiKey)) {
470
+ die('--card needs the harness\'s own FlareDeck credentials: flaredeck-agent login --tenant <id> --api-key <key>');
471
+ }
472
+ if (card)
473
+ say(`· approvals go to card ${card}`);
474
+ // Same path as the daemon: discover FlareDeck first, serve its tools from in here. A one-off
475
+ // run that cannot reach FlareDeck says so and stops, rather than answering without it.
476
+ let flaredeck;
477
+ if (config.oauth) {
478
+ const check = await preflightFlaredeck(config.mcpUrl, await harnessToken(config));
479
+ if (!check.ok) {
480
+ die(check.reason);
481
+ throw new Error(check.reason);
482
+ }
483
+ flaredeck = flaredeckProxy(config.mcpServerName, config.mcpUrl, () => harnessToken(config), check.discovered, (w) => say(`⚠ ${w}`));
484
+ }
485
+ const options = buildOptions(config, deciderFor(argv, config), {}, undefined, undefined, config.oauth ? await harnessToken(config) : undefined, undefined, flaredeck);
486
+ for await (const event of runTask(prompt, options)) {
487
+ if (event.kind === 'ready') {
488
+ // 'none' = no API key in use, i.e. the subscription. Anything else means this session
489
+ // would be billed per token, so stop rather than quietly run up a bill overnight.
490
+ if (event.apiKeySource !== 'none' && event.apiKeySource !== 'unknown') {
491
+ die(`Refusing to run: Claude is using ${event.apiKeySource}, not the subscription.`);
492
+ }
493
+ if (event.permissionMode !== 'auto') {
494
+ die(`Refusing to run: asked for auto mode, got "${event.permissionMode}". ` +
495
+ `A session that cannot use auto mode falls back to prompting, and there is nobody here to answer.`);
496
+ }
497
+ const bad = event.mcp.filter((s) => s.status === 'failed' || s.status === 'needs-auth');
498
+ for (const s of bad) {
499
+ // Worth shouting about: the agent would otherwise carry on with no Flaredeck tools and
500
+ // look perfectly healthy from the outside.
501
+ say(`⚠ MCP server "${s.name}" is ${s.status}. Re-run: claude mcp login ${s.name} --no-browser`);
502
+ }
503
+ if (event.tools.length === 0) {
504
+ // A session with no Flaredeck tools can still edit code and look fine, but it cannot read
505
+ // the card it was sent to work on. Silent is the worst way for that to go wrong.
506
+ say('⚠ no FlareDeck tools in this session — run: flaredeck-agent login --repo <this repo>');
507
+ }
508
+ say(`· ${event.permissionMode} mode · ${event.tools.length} Flaredeck tools`);
509
+ }
510
+ else if (event.kind === 'todo') {
511
+ say(` ☐ ${event.text}`);
512
+ }
513
+ else if (event.kind === 'tool') {
514
+ say(`· ${event.name}`);
515
+ }
516
+ else if (event.kind === 'text') {
517
+ say(event.text);
518
+ }
519
+ else if (event.kind === 'done') {
520
+ if (!event.ok)
521
+ die('\nThe task did not finish cleanly.');
522
+ }
523
+ }
524
+ }
525
+ /**
526
+ * The whole point of this program: stay up and work what FlareDeck sends. So running it with no
527
+ * arguments does that, in the repo you are standing in — the way `claude` starts Claude Code
528
+ * rather than printing a menu. Anything not set up yet is set up first, rather than being
529
+ * reported as an error for you to go and fix.
530
+ */
531
+ /**
532
+ * Credentials the agent may use and never see.
533
+ *
534
+ * Typed HERE, in this process, with the terminal's echo off — not into the agent's pane, where it
535
+ * would be in the transcript, on disk and in every request that followed. The agent is told only
536
+ * the NAME, and writes `{{secret:NAME}}` where the value belongs.
537
+ */
538
+ async function cmdSecret(argv) {
539
+ const [action, name] = argv.filter((a) => !a.startsWith('--'));
540
+ if (!action || action === 'list') {
541
+ const names = secretNames();
542
+ say(names.length ? `Secrets on this machine:\n${names.map((n) => ` ${n}`).join('\n')}` : 'No secrets on this machine yet.');
543
+ say(`\nStored in ${secretsPath()} (0600). An agent writes {{secret:NAME}} and never sees the value.`);
544
+ return;
545
+ }
546
+ if (action === 'rm' || action === 'remove') {
547
+ if (!name) {
548
+ die('Usage: flaredeck-agent secret rm <NAME>');
549
+ return;
550
+ }
551
+ say(removeSecret(name) ? `Removed ${name}.` : `There is no secret called ${name}.`);
552
+ return;
553
+ }
554
+ if (action !== 'add' && action !== 'set')
555
+ die('Usage: flaredeck-agent secret [list | add <NAME> | rm <NAME>]');
556
+ if (!name) {
557
+ die('Usage: flaredeck-agent secret add <NAME> (e.g. STRIPE_KEY)');
558
+ return;
559
+ }
560
+ // Piped input is for scripts and for a value too long to type; otherwise ask with echo off so
561
+ // it is not left on screen, in scrollback, or in the shell history of whoever is watching.
562
+ const piped = !process.stdin.isTTY ? await readAll(process.stdin) : null;
563
+ const value = piped !== null ? piped.trim() : await askHidden(`Value for ${name} (not shown): `);
564
+ try {
565
+ setSecret(name, value);
566
+ }
567
+ catch (e) {
568
+ die(e instanceof Error ? e.message : String(e));
569
+ }
570
+ say(`Saved ${name}. Agents on this machine can use it as {{secret:${name}}} — they cannot read it.`);
571
+ }
572
+ /** Read a line with the terminal's echo off, so the value never appears on screen. */
573
+ async function askHidden(prompt) {
574
+ process.stdout.write(prompt);
575
+ const stdin = process.stdin;
576
+ const wasRaw = stdin.isRaw ?? false;
577
+ stdin.setRawMode?.(true);
578
+ stdin.resume();
579
+ let out = '';
580
+ try {
581
+ for await (const chunk of stdin) {
582
+ const text = String(chunk);
583
+ // Ctrl-C and Ctrl-D must still get out of here rather than being typed into the value.
584
+ if (text.includes('\u0003')) {
585
+ process.stdout.write('\n');
586
+ process.exit(130);
587
+ }
588
+ if (text.includes('\u0004') && !out) {
589
+ process.stdout.write('\n');
590
+ process.exit(130);
591
+ }
592
+ if (text.includes('\r') || text.includes('\n')) {
593
+ out += text.split(/[\r\n]/)[0] ?? '';
594
+ break;
595
+ }
596
+ if (text === '\u007f') {
597
+ out = out.slice(0, -1);
598
+ continue;
599
+ }
600
+ out += text;
601
+ }
602
+ }
603
+ finally {
604
+ stdin.setRawMode?.(wasRaw);
605
+ stdin.pause();
606
+ process.stdout.write('\n');
607
+ }
608
+ return out;
609
+ }
610
+ async function readAll(stream) {
611
+ let out = '';
612
+ for await (const chunk of stream)
613
+ out += String(chunk);
614
+ return out;
615
+ }
616
+ async function cmdServe(argv) {
617
+ const repo = resolve(flag(argv, '--repo') ?? process.cwd());
618
+ let config = { ...configOrDefaults(), repo };
619
+ // Which model answers. `--agy` is the short way to try Gemini on one run; `--engine <name>` is
620
+ // the explicit one. Set on the environment rather than on the config so it applies to THIS run
621
+ // and nothing is left behind in a file for the next one to inherit silently — the repo's own
622
+ // `engine:` stays the default, which is what you come back to.
623
+ const engine = argv.includes('--agy') ? 'antigravity' : flag(argv, '--engine');
624
+ if (engine)
625
+ process.env['FLAREDECK_AGENT_ENGINE'] = engine;
626
+ const usingClaude = (engine ?? config.engine ?? 'claude') === 'claude';
627
+ // Resolved here rather than on the first card: an agent that starts, listens, takes a card and
628
+ // only then finds it has no model is worse than one that refuses to start. It ASKS for nothing
629
+ // — a key already in the providers file, or the gcloud login this machine already has, and it
630
+ // only complains when neither exists.
631
+ if (!usingClaude) {
632
+ try {
633
+ const { geminiEnv } = await import('./engine-antigravity.js');
634
+ geminiEnv();
635
+ }
636
+ catch (e) {
637
+ die(e instanceof Error ? e.message : String(e));
638
+ }
639
+ }
640
+ // Only a genuine absence of credentials triggers setup. It used to be any answer other than
641
+ // 'subscription', which meant a flaky probe sent a working machine back through login on every
642
+ // start — see claudeAuth().
643
+ // Claude's own login is only needed by the Claude engine. A machine running Gemini should not be
644
+ // sent through it — it has nothing to sign into.
645
+ if ((usingClaude && (await claudeAuth()).kind === 'none') || !config.oauth) {
646
+ say('Not set up in this repo yet — doing that first.\n');
647
+ await cmdLogin([...argv, '--repo', repo]);
648
+ config = loadConfig() ?? config;
649
+ }
650
+ if (config.repo !== repo) {
651
+ // Standing somewhere else is a decision, not a mistake, but it must be visible: an agent
652
+ // silently working a different checkout than the one you are looking at is a bad hour.
653
+ say(`· working ${repo} (config had ${config.repo})`);
654
+ config = { ...config, repo };
655
+ }
656
+ if (!config.oauth) {
657
+ die('This machine is not authorised yet. Run: flaredeck-agent login');
658
+ }
659
+ // A function, not a value: the access token lasts an hour and the daemon runs for weeks, so
660
+ // it is refreshed on use and the new refresh token written back — the provider re-issues it
661
+ // each time, and dropping that would silently end the agent's life in thirty days.
662
+ const auth = () => freshAccessToken(mcpOrigin(config), config.oauth, (next) => {
663
+ config.oauth = next;
664
+ saveConfig(config);
665
+ });
666
+ const tenantId = tenantOf(config);
667
+ // A person logged in here can answer the agent's requests from its pane. Optional: without it
668
+ // the pane still shows the request and says where else it can be answered.
669
+ const me = meClient(config);
670
+ return serve({
671
+ config,
672
+ agent: identityOf(config) ?? 'flaredeck-agent',
673
+ auth,
674
+ baseUrl: config.agentServiceUrl ?? DEFAULTS.agentServiceUrl,
675
+ decider: deciderFor(argv, config),
676
+ // The harness's own client, for what the model's tools must not do for themselves — posting
677
+ // an approval request and reading the answer.
678
+ ...(tenantId ? { deck: new Flaredeck(tenantId, auth) } : {}),
679
+ ...(me ? { me } : {}),
680
+ say,
681
+ });
682
+ }
683
+ async function main() {
684
+ const [, , first, ...rest] = process.argv;
685
+ // No arguments, or straight into flags: serve. A bare `flaredeck-agent` is the normal case.
686
+ const known = ['login', 'logout', 'whoami', 'start', 'serve', 'run', 'approvals', 'providers', 'secret', 'intake', 'help'];
687
+ // A bare invocation serves. Anything unrecognised must NOT: `--help` quietly starting a daemon
688
+ // is how someone ends up with an agent running that they meant to ask a question of.
689
+ const help = ['--help', '-h', 'help', '--version', '-v'];
690
+ // Flags that may come FIRST, because they are options to the bare serve: `flaredeck-agent --agy`
691
+ // has to work without typing `serve`. Named one by one rather than accepting any leading flag —
692
+ // a mistyped `--agi` must still be refused, for the reason directly above.
693
+ const serveFlags = ['--agy', '--engine', '--repo'];
694
+ const command = !first ? 'serve'
695
+ : help.includes(first) ? 'help'
696
+ : known.includes(first) ? first
697
+ : serveFlags.includes(first) ? 'serve'
698
+ : 'unknown';
699
+ if (command === 'unknown')
700
+ die(`Not a command: ${first}\nRun \`flaredeck-agent help\` to see what is.`);
701
+ const argv = first && known.includes(first) ? rest : process.argv.slice(2);
702
+ if (command === 'login')
703
+ return argv.includes('--jam') ? cmdLoginJam() : argv.includes('--me') ? cmdLoginMe() : cmdLogin(argv);
704
+ if (command === 'approvals')
705
+ return cmdApprovals(argv);
706
+ if (command === 'providers')
707
+ return cmdProviders(argv);
708
+ if (command === 'secret')
709
+ return cmdSecret(argv);
710
+ if (command === 'intake')
711
+ return cmdIntake(argv);
712
+ if (command === 'logout') {
713
+ const repo = resolve(flag(argv, '--repo') ?? process.cwd());
714
+ const config = loadConfig(repo);
715
+ say(forgetConfig(repo)
716
+ ? `Forgot the agent for ${repo}. Run login to set one up again.`
717
+ : `No agent was set up for ${repo}.`);
718
+ return;
719
+ }
720
+ if (command === 'whoami') {
721
+ const auth = await claudeAuth();
722
+ say(`Claude: ${auth.kind} — ${auth.detail}\n`);
723
+ return cmdRun('Call hello-authenticated and tell me, in one line, which account I am acting as.', argv);
724
+ }
725
+ // What an agent does when it wakes up with no instruction: find out who it is and what is
726
+ // waiting for it. The same thing the always-on daemon will do on every FlareDeck event.
727
+ if (command === 'start') {
728
+ return cmdRun('Get your bearings, then stop and report — do not start any of the work yet.\n\n' +
729
+ '1. Call hello-authenticated so you know which account you are.\n' +
730
+ '2. Call get-outstanding to find the cards assigned to you that are not done.\n' +
731
+ '3. Put one todo on your list per card, in the order you would tackle them.\n' +
732
+ '4. Tell me, in a few lines: who you are, how many cards are waiting, and which you would ' +
733
+ 'pick up first and why.', argv);
734
+ }
735
+ if (command === 'serve')
736
+ return cmdServe(argv);
737
+ if (command === 'run') {
738
+ const prompt = argv.filter((a) => !a.startsWith('--')).join(' ');
739
+ if (!prompt)
740
+ die('Usage: flaredeck-agent run "<what to do>" [--ask]');
741
+ return cmdRun(prompt, argv);
742
+ }
743
+ say('flaredeck-agent — a FlareDeck agent on this machine\n');
744
+ say(' (no arguments) Stay up and work what FlareDeck sends, in this repo (Claude)');
745
+ say(' --agy The same, with Gemini answering instead of Claude');
746
+ say(' (--engine <claude|antigravity> for the long form)');
747
+ say(' login [--repo <path>] [--tenant <id>] [--api-key <key>]');
748
+ say(' Authenticate with Claude and FlareDeck (once per machine)');
749
+ say(' login --me Sign in as YOURSELF, to answer agents\' requests from here');
750
+ say(' approvals [--tenant <id>] Every request agents are waiting on; approve or deny from the keyboard');
751
+ say(' providers [--openrouter <key>] [--gemini <key>] [--brief haiku|gemini] Keys for the optional small models intake may use (machine-wide)');
752
+ say(' intake <card> Dry-run the intake step on a card: digest size, the brief, Jev beside it, the model it would pick');
753
+ say(' login --jam Connect Jam so intake can read recordings linked on cards (transcript, console, failed requests)');
754
+ say(' login --github-token <pat> Use a classic personal access token for pushes instead of the OAuth login (orgs that restrict OAuth apps)');
755
+ say(' login --github Redo only the GitHub login on this machine (lapsed or revoked refresh token)');
756
+ say(' logout [--repo <path>] Forget this repo\'s agent, so you can log in as another');
757
+ say(' whoami Show which Claude auth and which FlareDeck account is in use');
758
+ say(' start [--card <code>] Find out who you are and what work is waiting');
759
+ say(' run "<task>" [options] Run one task');
760
+ say(' --card <code> Ask for approval on this card; anyone on the board can answer');
761
+ say(' --ask Prompt at the terminal instead (hand-testing)');
762
+ }
763
+ main().catch((e) => die(String(e instanceof Error ? e.message : e)));
764
+ //# sourceMappingURL=main.js.map