teamshare-bridge 0.2.0 → 0.2.2

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 (78) hide show
  1. package/README.md +42 -13
  2. package/dist/bridge/daemon.js +56 -17
  3. package/dist/bridge/daemon.js.map +1 -1
  4. package/dist/bridge/spawn.d.ts +12 -0
  5. package/dist/bridge/spawn.js +11 -5
  6. package/dist/bridge/spawn.js.map +1 -1
  7. package/dist/cli/args.d.ts +8 -0
  8. package/dist/cli/args.js +38 -0
  9. package/dist/cli/args.js.map +1 -0
  10. package/dist/cli/commands/build.d.ts +1 -0
  11. package/dist/cli/commands/build.js +385 -0
  12. package/dist/cli/commands/build.js.map +1 -0
  13. package/dist/cli/commands/chat.d.ts +1 -0
  14. package/dist/cli/commands/chat.js +113 -0
  15. package/dist/cli/commands/chat.js.map +1 -0
  16. package/dist/cli/commands/connect.d.ts +1 -0
  17. package/dist/cli/commands/connect.js +23 -0
  18. package/dist/cli/commands/connect.js.map +1 -0
  19. package/dist/cli/commands/doc.d.ts +1 -0
  20. package/dist/cli/commands/doc.js +109 -0
  21. package/dist/cli/commands/doc.js.map +1 -0
  22. package/dist/cli/commands/harness.d.ts +10 -0
  23. package/dist/cli/commands/harness.js +93 -0
  24. package/dist/cli/commands/harness.js.map +1 -0
  25. package/dist/cli/commands/key.d.ts +2 -0
  26. package/dist/cli/commands/key.js +126 -0
  27. package/dist/cli/commands/key.js.map +1 -0
  28. package/dist/cli/commands/link.d.ts +2 -0
  29. package/dist/cli/commands/link.js +93 -0
  30. package/dist/cli/commands/link.js.map +1 -0
  31. package/dist/cli/commands/run.d.ts +1 -0
  32. package/dist/cli/commands/run.js +219 -0
  33. package/dist/cli/commands/run.js.map +1 -0
  34. package/dist/cli/commands/shared.d.ts +6 -0
  35. package/dist/cli/commands/shared.js +51 -0
  36. package/dist/cli/commands/shared.js.map +1 -0
  37. package/dist/cli/commands/skills.d.ts +1 -0
  38. package/dist/cli/commands/skills.js +29 -0
  39. package/dist/cli/commands/skills.js.map +1 -0
  40. package/dist/cli/commands/usage.d.ts +1 -0
  41. package/dist/cli/commands/usage.js +78 -0
  42. package/dist/cli/commands/usage.js.map +1 -0
  43. package/dist/cli/index.js +49 -1500
  44. package/dist/cli/index.js.map +1 -1
  45. package/dist/cli/utils.d.ts +38 -0
  46. package/dist/cli/utils.js +139 -0
  47. package/dist/cli/utils.js.map +1 -0
  48. package/dist/lib/api.d.ts +15 -2
  49. package/dist/lib/api.js +8 -4
  50. package/dist/lib/api.js.map +1 -1
  51. package/dist/lib/brief.js +19 -3
  52. package/dist/lib/brief.js.map +1 -1
  53. package/dist/lib/config.d.ts +3 -1
  54. package/dist/lib/config.js +4 -2
  55. package/dist/lib/config.js.map +1 -1
  56. package/dist/lib/harness.d.ts +1 -1
  57. package/dist/lib/harness.js +3 -2
  58. package/dist/lib/harness.js.map +1 -1
  59. package/dist/lib/llm.js +8 -7
  60. package/dist/lib/llm.js.map +1 -1
  61. package/dist/lib/models.d.ts +6 -0
  62. package/dist/lib/models.js +25 -0
  63. package/dist/lib/models.js.map +1 -1
  64. package/dist/lib/orchestrator/client.d.ts +38 -4
  65. package/dist/lib/orchestrator/client.js +390 -22
  66. package/dist/lib/orchestrator/client.js.map +1 -1
  67. package/dist/lib/orchestrator/index.d.ts +1 -1
  68. package/dist/lib/orchestrator/index.js +5 -1
  69. package/dist/lib/orchestrator/index.js.map +1 -1
  70. package/dist/lib/orchestrator/worktree.d.ts +52 -0
  71. package/dist/lib/orchestrator/worktree.js +250 -6
  72. package/dist/lib/orchestrator/worktree.js.map +1 -1
  73. package/dist/lib/skills.d.ts +2 -2
  74. package/dist/lib/skills.js +2 -1
  75. package/dist/lib/skills.js.map +1 -1
  76. package/package.json +1 -1
  77. package/skills/teamshare-auth-rbac/SKILL.md +1 -1
  78. package/skills/teamshare-orchestrator-ask/SKILL.md +63 -0
package/dist/cli/index.js CHANGED
@@ -4,1525 +4,75 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  /**
5
5
  * teamshare-agent - the TeamShare agent CLI.
6
6
  *
7
- * Commands:
8
- * connect --agent <id> --key <ts_...> test + save credentials
9
- * run --task <taskId> --agent <id> --key <ts_...> [--harness ...] [--self] [--no-report] [--session <id>]
10
- * chat --project <projectId> --agent <id> --key <ts_...> [--mention <token>] [--session <id>]
11
- * Reply to every chat message that @mentions the agent.
12
- * --help
13
- *
14
- * Every session streams its output to the live console (Phase A): the CLI
15
- * opens its own /agents WebSocket and emits session:start / session:output
16
- * (batched) / session:end. Streaming is best-effort - it degrades silently.
7
+ * Modular structure:
8
+ * cli/index.ts - entry point, arg parsing, command routing
9
+ * cli/utils.ts - shared helpers (model, harness, settings, logging)
10
+ * cli/harness.ts - spawn harness (opencode/claude)
11
+ * cli/commands/*.ts - individual commands (connect, link, run, chat, doc, build, usage, skills, key)
17
12
  */
18
- const node_child_process_1 = require("node:child_process");
19
- const node_fs_1 = require("node:fs");
20
- const node_path_1 = require("node:path");
21
- const node_os_1 = require("node:os");
22
- const node_fs_2 = require("node:fs");
13
+ const args_1 = require("./args");
23
14
  const api_1 = require("../lib/api");
24
- const brief_1 = require("../lib/brief");
25
- const llm_1 = require("../lib/llm");
26
- const chat_reply_1 = require("../lib/chat-reply");
27
- const doc_reply_1 = require("../lib/doc-reply");
28
15
  const config_1 = require("../lib/config");
29
- const lock_1 = require("../lib/lock");
30
- const usage_1 = require("../lib/usage");
31
- const session_stream_1 = require("../lib/session-stream");
32
- const models_1 = require("../lib/models");
33
- const harness_1 = require("../lib/harness");
34
- const skills_1 = require("../lib/skills");
35
- const VERSION = '0.1.0';
36
- const WS_DEFAULT = 'ws://localhost:4000';
37
- /** Build mode (IMP-700): the project chat channel for agent questions. */
38
- const AGENT_QUESTIONS_CHANNEL = 'agent-questions';
39
- /** Per-project build run state (skipped tasks awaiting answers) lives here. */
40
- const BUILD_STATE_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.teamshare', 'builds');
41
- /** Stop file: the daemon writes it on build.stop; the loop honors + deletes it. */
42
- const STOP_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.teamshare', 'stops');
43
- /** Best-effort fetch of the agent's stored settings - never fatal. */
44
- async function fetchAgentSettings(api, agentId, log) {
45
- try {
46
- const s = await api.getAgentSettings(agentId);
47
- log(`[settings] agent stored: model=${s.model ?? 'none'} temperature=${s.temperature ?? 'default'} maxTokens=${s.maxTokens ?? 'default'} harness=${s.harness ?? 'default'} prompt=${s.systemPrompt ? 'set' : 'none'} providerKey=${s.providerKey ? 'set' : 'none'}`);
48
- return {
49
- model: s.model ?? undefined,
50
- temperature: s.temperature ?? undefined,
51
- maxTokens: s.maxTokens ?? undefined,
52
- systemPrompt: s.systemPrompt,
53
- userId: s.userId ?? undefined,
54
- harness: s.harness,
55
- providerKey: s.providerKey,
56
- };
57
- }
58
- catch (err) {
59
- log(`[settings] could not fetch agent settings (${(0, config_1.msg)(err)}) - using flags/env`, 'stderr');
60
- return {};
61
- }
62
- }
63
- /** Session driver resolution: flag > agent setting > local config > auto. */
64
- function harnessOf(flags, agent) {
65
- return (flags.get('harness') ??
66
- agent?.harness ??
67
- (0, config_1.loadConfig)().harness ??
68
- 'auto');
69
- }
70
- /**
71
- * LLM provider key for self-loop / chat / doc sessions:
72
- * server BYOK (settings.providerKey) > local config chatKey >
73
- * CHAT_LLM_API_KEY > LLM_API_KEY env. Never logged.
74
- */
75
- function sessionLlmApiKey(agent) {
76
- return agent?.providerKey ?? (0, config_1.chatKey)() ?? '';
77
- }
78
- /** Phase D settings resolution: CLI flag > agent settings > env > default. */
79
- function modelOf(flags, agent) {
80
- return (flags.get('model') ??
81
- agent?.model ??
82
- process.env.LLM_MODEL ??
83
- llm_1.DEFAULT_LLM_MODEL);
84
- }
85
- /** Model spec -> { baseUrl, model } via the catalog (env LLM_BASE_URL wins). */
86
- function llmEndpoint(flags, agent) {
87
- const spec = modelOf(flags, agent);
88
- const resolved = (0, models_1.resolveModel)(spec, process.env.LLM_BASE_URL);
89
- if (resolved.harnessOnly) {
90
- console.log(`[model] "${spec}" is a harness-native model - must use opencode/claude harness, not --self`);
91
- }
92
- else if (!resolved.catalogued) {
93
- console.log(`[model] "${spec}" is not a catalogued provider:model spec - using LLM_BASE_URL (${resolved.baseUrl})`);
94
- }
95
- return { baseUrl: resolved.baseUrl, model: resolved.model, harnessOnly: resolved.harnessOnly };
96
- }
97
- /**
98
- * Applies the LLM provider key for harness runs: server BYOK
99
- * (settings.providerKey) > per-agent key > global key (both from
100
- * ~/.teamshare/config.json). Set as OPENCODE_API_KEY so the harness child
101
- * inherits it - agent usage never bills the user's default opencode
102
- * credentials. An env var already set always wins. The KEY VALUE IS NEVER
103
- * LOGGED - only which source was used.
104
- */
105
- function applyOpenCodeKey(agentId, log, settings) {
106
- if (process.env.OPENCODE_API_KEY) {
107
- log('[key] OPENCODE_API_KEY env already set - leaving it untouched');
108
- return;
109
- }
110
- const key = settings?.providerKey ?? (0, config_1.agentOpenCodeKey)(agentId);
111
- if (!key) {
112
- log('[key] no LLM provider key configured - opencode falls back to auth.json');
113
- return;
114
- }
115
- process.env.OPENCODE_API_KEY = key;
116
- log(settings?.providerKey
117
- ? '[key] server BYOK provider key applied (isolated from your default credentials)'
118
- : '[key] local agent LLM provider key applied (isolated from your default credentials)');
119
- }
120
- /** Harness kill-switch: max minutes an opencode/claude child may run (env TEAMSHARE_SESSION_TIMEOUT_MIN, default 60). */
121
- function sessionTimeoutMin(flags) {
122
- const raw = flags.get('session-timeout') ?? process.env.TEAMSHARE_SESSION_TIMEOUT_MIN;
123
- const n = Number(raw);
124
- return Number.isFinite(n) && n > 0 ? n : 60;
125
- }
126
- function temperatureOf(flags, agent) {
127
- const raw = flags.get('temperature') ??
128
- (agent?.temperature !== undefined ? String(agent.temperature) : undefined) ??
129
- process.env.LLM_TEMPERATURE;
130
- if (raw === undefined)
131
- return undefined;
132
- const n = Number(raw);
133
- return Number.isFinite(n) ? Math.min(2, Math.max(0, n)) : undefined;
134
- }
135
- function maxTokensOf(flags, agent) {
136
- const raw = flags.get('max-tokens') ??
137
- (agent?.maxTokens !== undefined ? String(agent.maxTokens) : undefined) ??
138
- process.env.LLM_MAX_TOKENS;
139
- if (raw === undefined)
140
- return undefined;
141
- const n = Number(raw);
142
- return Number.isInteger(n) && n > 0 ? n : undefined;
143
- }
144
- function systemPromptOf(flags, agent) {
145
- const inline = flags.get('system-prompt');
146
- if (inline !== undefined)
147
- return inline;
148
- const file = flags.get('system-prompt-file');
149
- if (file && (0, node_fs_1.existsSync)(file))
150
- return (0, node_fs_1.readFileSync)(file, 'utf8');
151
- return agent?.systemPrompt ?? null;
152
- }
153
- /** Console + live-stream logger for a session. */
154
- function makeLog(stream) {
155
- return (text, level = 'stdout') => {
156
- console[level === 'stderr' ? 'error' : 'log'](text);
157
- stream?.line(text, level);
158
- };
159
- }
16
+ // Command imports
17
+ const connect_1 = require("./commands/connect");
18
+ const link_1 = require("./commands/link");
19
+ const run_1 = require("./commands/run");
20
+ const chat_1 = require("./commands/chat");
21
+ const doc_1 = require("./commands/doc");
22
+ const build_1 = require("./commands/build");
23
+ const usage_1 = require("./commands/usage");
24
+ const skills_1 = require("./commands/skills");
25
+ const key_1 = require("./commands/key");
26
+ const WS_DEFAULT = api_1.DEFAULT_WS_URL;
160
27
  function usage() {
161
28
  return [
162
29
  'teamshare-agent - TeamShare agent CLI',
163
30
  '',
164
31
  'USAGE',
165
32
  ' teamshare-agent connect --agent <agentId> --key <ts_...>',
166
- ' Test the connection and save the agent key in ~/.teamshare/config.json.',
167
- '',
168
- ' teamshare-agent link --project <projectId> --path <folder> [--guard true|false]',
169
- ' PROJECT GUARD: bind a TeamShare project to a local folder. The agent',
170
- ' refuses to work a project until it is linked, and every session then',
171
- ' runs inside that folder (read-only warning for OneDrive/network/',
172
- ' outside-profile paths - those are blocked).',
33
+ ' teamshare-agent link --project <projectId> --path <folder> [--guard true|false] [--origin <url>]',
173
34
  ' teamshare-agent link --list',
174
35
  ' teamshare-agent unlink --project <projectId>',
175
- ' Remove the link - sessions for that project are blocked again.',
176
- '',
177
36
  ' teamshare-agent run --task <taskId> --agent <agentId> --key <ts_...> [options]',
178
- ' Fetch the task brief and work it with a harness or headless LLM.',
179
- ' Without --task, picks up the agent\'s next assigned task from its inbox',
180
- ' (urgent > high > medium > low, then oldest; skips in_review/resolved/closed).',
181
- '',
182
37
  ' teamshare-agent continue --task <taskId> --agent <agentId> --key <ts_...> [--self]',
183
- ' Resume the previous --self session for a task (Phase E continuation).',
184
- '',
185
- ' teamshare-agent chat --project <projectId> --agent <agentId> --key <ts_...> [--mention <token>] [--target <messageId>]',
186
- ' Reply to ONE project-chat message that @mentions the agent (one mention',
187
- ' → one reply → sleep). --target pins the reply to that specific message;',
188
- ' without it the latest mention is used. Spawned by the daemon on chat.mention wakes.',
189
- '',
38
+ ' teamshare-agent chat --project <projectId> --agent <agentId> --key <ts_...> [--mention <token>]',
190
39
  ' teamshare-agent doc --document <documentId> --agent <agentId> --key <ts_...> [--mention <token>]',
191
- ' Document assistant (Phase F): read the document (Word/PPT/Excel/PDF)',
192
- ' and reply to every comment that @mentions the agent in its thread.',
193
- ' Spawned by the daemon on document.mention wakes.',
194
- '',
195
40
  ' teamshare-agent build --project <projectId> --agent <agentId> --key <ts_...> [options]',
196
- ' BUILD MODE: work the project\'s ready-for-dev queue in strict order',
197
- ' (sortOrder asc, createdAt asc), one task at a time, fully completing',
198
- ' each. A failed task is skipped: a [question] comment + #agent-questions',
199
- ' message ask the team what to do, the build continues, and the task is',
200
- ' resumed automatically once a human answers. Pass --once to stop after',
201
- ' the first task; --dry-run prints the queue without working anything.',
202
- ' teamshare-agent build --project <id> --agent <id> --key <ts_...> --draft "<goal>" [--go]',
203
- ' Draft the build queue for a goal: the agent plans 3-8 concrete tasks,',
204
- ' creates them ready-for-dev in order and assigns them to itself. --go',
205
- ' executes the queue immediately afterwards; without it just the plan.',
206
- '',
207
41
  ' teamshare-agent usage [--day YYYY-MM-DD] [--agent <agentId>]',
208
- ' Agent spend report - read-only daily cost/token view of the local',
209
- ' opencode db (~/.local/share/opencode/opencode.db). Offline; needs',
210
- ' neither the backend nor the daemon. Attribution: "TeamShare "',
211
- ' titles = agent sessions, providerID opencode = your own windows.',
212
- '',
213
42
  ' teamshare-agent skills install|list|uninstall',
214
- ' Install the TeamShare agent skills (teamshare-cdd, teamshare-modularity,',
215
- ' teamshare-auth-rbac) into the opencode skills directory (~/.agents/skills',
216
- ' by default; TEAMSHARE_SKILLS_DIR overrides). Agents working TeamShare tasks',
217
- ' are expected to load them and follow their rules.',
218
- '',
219
43
  ' teamshare-agent key set --key <providerApiKey> [--agent <agentId>]',
220
44
  ' teamshare-agent key show | clear [--agent <agentId>]',
221
- ' LLM provider key for agent harness runs (e.g. an opencode-go API key).',
222
- ' Without --agent it is the global default for every agent; with --agent',
223
- ' it overrides that agent only. Agents run with their OWN key so usage',
224
- ' never bills your default opencode credentials. Keys are never printed.',
225
45
  '',
226
46
  'OPTIONS',
227
47
  ' --harness auto|opencode|claude|none session driver (default: config or auto)',
228
48
  ' --self headless LLM loop (needs LLM_API_KEY env)',
229
49
  ' --no-report skip the closing summary comment',
230
- ' --continue resume the previous --self session for this task (Phase E)',
50
+ ' --continue resume previous --self session',
231
51
  ' --session <id> live-console session id (default: auto)',
232
- ' --cwd <folder> work inside this folder (else the linked',
233
- ' project folder; hard-blocked if neither)',
234
- ' --model <id> model override (Phase D; env LLM_MODEL)',
235
- ' --temperature <0-2> sampling temperature (Phase D; env LLM_TEMPERATURE)',
236
- ' --max-tokens <n> token budget (Phase D; env LLM_MAX_TOKENS)',
237
- ' --system-prompt <text> system prompt override (Phase D)',
238
- ' --system-prompt-file <path> system prompt from a file (Phase D)',
239
- ' --ask-timeout <sec> ask_human wait limit (Phase B; env TEAMSHARE_ASK_TIMEOUT)',
240
- ' --session-timeout <min> harness kill-switch cap (default 60; env TEAMSHARE_SESSION_TIMEOUT_MIN)',
241
- ' --once build: stop after the first task',
242
- ' --dry-run build: print the queue, work nothing',
243
- ' --draft <goal> build: plan the queue for a goal (creates tasks)',
244
- ' --go build: execute the queue after --draft',
245
- ' --on-fail ask|stop build: ask a question (resume later) or stop the run on failure (default ask)',
246
- ' --blocked-wait <min> build: how long to wait for answers before exiting (default 30)',
247
- ' --api-url <url> TeamShare API base (default: ' + api_1.DEFAULT_API_URL + ')',
248
- ' --day <YYYY-MM-DD> usage report day (default: today)',
249
- ' --agent <agentId> usage report filter (title contains "agent <id>")',
250
- ' -h, --help this help',
52
+ ' --cwd <folder> work inside this folder',
53
+ ' --model <id> model override',
54
+ ' --temperature <0-2> sampling temperature',
55
+ ' --max-tokens <n> token budget',
56
+ ' --system-prompt <text> system prompt override',
57
+ ' --session-timeout <min> harness kill-switch (default 60)',
58
+ ' --once build: stop after first task',
59
+ ' --dry-run build: print queue, work nothing',
60
+ ' --draft <goal> build: plan queue for a goal',
61
+ ' --go build: execute after --draft',
62
+ ' --on-fail ask|stop build: action on failure (default ask)',
63
+ ' --api-url <url> API base (default: ' + api_1.DEFAULT_API_URL + ')',
64
+ ' -h, --help this help',
251
65
  '',
252
66
  'ENV',
253
67
  ' TEAMSHARE_API_URL API base override',
254
- ' TEAMSHARE_WS_URL /agents WebSocket base (default: ' + WS_DEFAULT + ')',
255
- ' LLM_BASE_URL OpenAI-compatible base (default: ' + llm_1.DEFAULT_LLM_BASE_URL + ')',
68
+ ' TEAMSHARE_WS_URL WebSocket base (default: ' + WS_DEFAULT + ')',
69
+ ' LLM_BASE_URL OpenAI-compatible base',
256
70
  ' LLM_API_KEY required for --self',
257
- ' LLM_MODEL model id (default: ' + llm_1.DEFAULT_LLM_MODEL + ')',
258
- '',
259
- 'EXAMPLES',
260
- ' teamshare-agent connect --agent 3f9a... --key ts_AbC123...',
261
- ' teamshare-agent run --task 8b2c... --agent 3f9a... --key ts_AbC123... --self',
71
+ ' LLM_MODEL model id (default: opencode-go/mimo-v2.5)',
262
72
  ].join('\n');
263
73
  }
264
- function parseArgs(argv) {
265
- const flags = new Map();
266
- const positionals = [];
267
- for (let i = 0; i < argv.length; i += 1) {
268
- const arg = argv[i];
269
- if (arg === '-h' || arg === '--help') {
270
- flags.set('help', 'true');
271
- }
272
- else if (arg.startsWith('--')) {
273
- const eq = arg.indexOf('=');
274
- if (eq > 0) {
275
- flags.set(arg.slice(2, eq), arg.slice(eq + 1));
276
- }
277
- else {
278
- const name = arg.slice(2);
279
- const value = argv[i + 1];
280
- if (value !== undefined && !value.startsWith('--')) {
281
- flags.set(name, value);
282
- i += 1;
283
- }
284
- else {
285
- flags.set(name, 'true');
286
- }
287
- }
288
- }
289
- else {
290
- positionals.push(arg);
291
- }
292
- }
293
- return { flags, positionals };
294
- }
295
- function need(flags, name) {
296
- const value = flags.get(name);
297
- if (!value) {
298
- console.error(`Missing required flag: --${name}`);
299
- process.exitCode = 2;
300
- throw new Error(`missing --${name}`);
301
- }
302
- return value;
303
- }
304
- /**
305
- * Project guard (IMP-6xx): resolve the working folder for a session. An
306
- * explicit `--cwd` wins; otherwise the project's linked folder from the
307
- * local config is used. Returns null (blocked) when no folder is available
308
- * - the caller must refuse the session.
309
- */
310
- function resolveWorkingDir(flags, projectId, log) {
311
- const explicit = flags.get('cwd');
312
- if (explicit) {
313
- const abs = (0, node_path_1.resolve)(explicit);
314
- if (!(0, node_fs_1.existsSync)(abs) || !(0, node_fs_1.statSync)(abs).isDirectory()) {
315
- log(`[guard] --cwd is not an existing folder: ${abs}`, 'stderr');
316
- return null;
317
- }
318
- log(`[guard] working folder: ${abs}`);
319
- return abs;
320
- }
321
- const cwd = projectId ? (0, config_1.projectCwd)(projectId) : null;
322
- if (!cwd) {
323
- log(projectId
324
- ? `[guard] BLOCKED - project ${projectId} has no linked folder.\n` +
325
- ` Link one first:\n ${(0, config_1.linkHint)(projectId)}`
326
- : `[guard] BLOCKED - could not determine the project; no folder to work in.\n` +
327
- ` Link one first:\n teamshare-agent link --project <projectId> --path C:\\path\\to\\folder`, 'stderr');
328
- return null;
329
- }
330
- log(`[guard] working folder: ${cwd}`);
331
- return cwd;
332
- }
333
- async function cmdConnect(flags) {
334
- const agentId = need(flags, 'agent');
335
- const apiKey = need(flags, 'key');
336
- const api = new api_1.TeamshareApi(process.env.TEAMSHARE_API_URL, apiKey);
337
- await api.heartbeat(agentId, 'online');
338
- (0, config_1.upsertAgent)(agentId, apiKey);
339
- console.log(`Connected: agent ${agentId} (key saved to ~/.teamshare/config.json)`);
340
- // Wire TeamShare MCP into opencode so the agent has tools in sessions.
341
- const wired = (0, config_1.wireOpencodeMcp)(apiKey);
342
- console.log(wired
343
- ? '[mcp] wired TeamShare MCP into opencode.jsonc — agent now has 21 tools (create_task, list_tasks, search, etc.)'
344
- : '[mcp] TeamShare MCP already wired in opencode.jsonc — skipping');
345
- console.log('Ready: teamshare-agent run --task <taskId> --agent ' + agentId + '');
346
- }
347
- /** `link` - project guard: bind a TeamShare project to a local folder. */
348
- async function cmdLink(flags) {
349
- if (flags.get('list') === 'true') {
350
- const entries = Object.entries((0, config_1.loadConfig)().projects);
351
- if (entries.length === 0) {
352
- console.log('No projects linked yet - run: teamshare-agent link --project <id> --path <folder>');
353
- return;
354
- }
355
- for (const [projectId, link] of entries) {
356
- console.log(`${projectId} -> ${link.path}${link.guard === false ? ' (guard off)' : ''}`);
357
- }
358
- return;
359
- }
360
- const projectId = need(flags, 'project');
361
- const path = need(flags, 'path');
362
- const result = (0, config_1.linkProject)(projectId, path, flags.get('guard') !== 'false');
363
- if (!result.ok) {
364
- console.error(`Could not link project ${projectId}:`);
365
- console.error(' ' + result.reason);
366
- process.exitCode = 1;
367
- return;
368
- }
369
- console.log(`Linked project ${projectId} -> ${result.canonical}`);
370
- console.log('The agent now works inside this folder for that project (guard on).');
371
- }
372
- /** `unlink` - remove a project link. */
373
- async function cmdUnlink(flags) {
374
- const projectId = need(flags, 'project');
375
- const config = (0, config_1.unlinkProject)(projectId);
376
- const gone = !config.projects[projectId];
377
- if (gone)
378
- console.log(`Unlinked project ${projectId} - its sessions are now blocked by the guard.`);
379
- else {
380
- console.error(`Project ${projectId} was not linked.`);
381
- process.exitCode = 1;
382
- }
383
- }
384
- /** `usage` - read-only daily spend report from the local opencode db. */
385
- async function cmdUsage(flags) {
386
- try {
387
- const report = (0, usage_1.runUsageReport)({
388
- day: flags.get('day') ?? undefined,
389
- agent: flags.get('agent') ?? undefined,
390
- });
391
- console.log(`Agent spend report - ${report.day} (local time)`);
392
- console.log(`Source: ${report.dbPath} (read-only)`);
393
- console.log(`Total: $${report.totalCost.toFixed(4)} across ${report.sessionCount} sessions`);
394
- console.log(` your own windows : $${report.userCost.toFixed(4)}`);
395
- console.log(` agent sessions : $${report.agentCost.toFixed(4)} (of which TeamShare $${report.teamshareCost.toFixed(4)})`);
396
- const t = report.tokens;
397
- console.log(`Tokens: ${t.input.toLocaleString()} in / ${t.output.toLocaleString()} out / ` +
398
- `${t.reasoning.toLocaleString()} reasoning / ${t.cacheRead.toLocaleString()} cache-read`);
399
- if (report.sessions.length === 0) {
400
- console.log('No cost-bearing sessions for this day.');
401
- return;
402
- }
403
- console.log('');
404
- console.log('time dur cost src model title');
405
- console.log('-'.repeat(120));
406
- for (const s of report.sessions) {
407
- const src = s.attribution === 'user' ? 'user' : s.teamshare ? 'teamshare' : 'agent';
408
- const start = new Date(s.timeCreated).toLocaleTimeString([], {
409
- hour: '2-digit',
410
- minute: '2-digit',
411
- });
412
- const end = new Date(s.timeUpdated).toLocaleTimeString([], {
413
- hour: '2-digit',
414
- minute: '2-digit',
415
- });
416
- const model = `${s.providerID}/${s.model}`.slice(0, 34);
417
- console.log(`${start}-${end} ${String(s.durationMin.toFixed(1)).padStart(5)}m ` +
418
- `${s.cost.toFixed(4).padStart(7)} ${src.padEnd(9)} ${model.padEnd(34)} ${s.title.slice(0, 60)}`);
419
- }
420
- console.log('');
421
- console.log(`Takeaway: agents cost $${report.agentCost.toFixed(4)} vs your own windows ` +
422
- `$${report.userCost.toFixed(4)} on ${report.day}.`);
423
- }
424
- catch (err) {
425
- console.error(`[usage] ${(0, config_1.msg)(err)}`);
426
- process.exitCode = 1;
427
- }
428
- }
429
- async function cmdRun(flags) {
430
- const agentId = need(flags, 'agent');
431
- const apiKey = need(flags, 'key');
432
- let selfMode = flags.get('self') === 'true';
433
- const noReport = flags.get('no-report') === 'true';
434
- const api = new api_1.TeamshareApi(process.env.TEAMSHARE_API_URL, apiKey);
435
- // Phase D: apply the agent's stored model/harness/providerKey/etc.
436
- // (flag > agent setting > env > default) for direct CLI sessions.
437
- const settings = await fetchAgentSettings(api, agentId, makeLog(null));
438
- const harness = harnessOf(flags, settings);
439
- // If model is harness-only (opencode-go), force harness mode instead of --self
440
- const modelSpec = modelOf(flags);
441
- const modelResolved = (0, models_1.resolveModel)(modelSpec, process.env.LLM_BASE_URL);
442
- if (modelResolved.harnessOnly && selfMode) {
443
- console.log(`[model] "${modelSpec}" is harness-native - switching from --self to harness mode`);
444
- selfMode = false;
445
- }
446
- // One session per agent at a time - wait for any in-flight session to finish.
447
- const lock = await (0, lock_1.acquireLock)(agentId, 'task', {
448
- taskId: flags.get('task') ?? undefined,
449
- }, {
450
- onWaiting: (busy) => console.log(`[lock] agent ${agentId} is busy (pid ${busy.pid}, since ${busy.startedAt}, ${busy.kind}) - waiting for it to finish...`),
451
- });
452
- if (!lock) {
453
- console.error(`[lock] gave up waiting for agent ${agentId} - try again later`);
454
- return;
455
- }
456
- let exitCode = 0;
457
- let heartbeatTimer;
458
- let claimedTaskId;
459
- const sessionId = flags.get('session') ?? (0, session_stream_1.newSessionId)();
460
- const stream = new session_stream_1.SessionStream({ sessionId, agentId, mode: 'run' });
461
- const log = makeLog(stream);
462
- applyOpenCodeKey(agentId, log, settings);
463
- try {
464
- // Explicit --task wins; otherwise pick up the agent's next assigned task
465
- // from its inbox (urgent > high > medium > low, then oldest; anything
466
- // in_review/resolved/closed is skipped).
467
- let taskId = flags.get('task');
468
- // Defense: a bare `--task` (no value) parses as boolean "true" via
469
- // parseArgs - never treat a non-UUID as a task id. Fall back to picking
470
- // the agent's next assigned task instead of failing every UUID check.
471
- if (taskId &&
472
- !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(taskId)) {
473
- log(`[pickup] --task ignored (not a UUID: "${taskId}") - picking the next assigned task`, 'stderr');
474
- taskId = undefined;
475
- }
476
- if (!taskId) {
477
- const picked = await pickNextTask(api, agentId, log);
478
- if (!picked) {
479
- log('[pickup] no assigned tasks to work - nothing to do');
480
- return;
481
- }
482
- taskId = picked;
483
- }
484
- // PROJECT GUARD (IMP-6xx): resolve the working folder for this task
485
- // before any work - unlinked projects are hard-blocked (the agent only
486
- // ever runs inside a folder you linked).
487
- const task = await api.getTask(taskId).catch((err) => {
488
- log(`[guard] FAILED to fetch task ${taskId}: ${err.message ?? err}`, 'stderr');
489
- return null;
490
- });
491
- if (!task) {
492
- log(`[guard] Cannot access task ${taskId} - stopping. Verify project membership.`, 'stderr');
493
- exitCode = 1;
494
- return;
495
- }
496
- stream.meta.projectId = task.projectId ?? undefined;
497
- stream.meta.taskId = taskId;
498
- // Connect the session stream now that the meta is known, so session:start
499
- // carries taskId/projectId (earlier lines replay from the buffer). Late
500
- // connect also means blocked/early-exit sessions still stream their exit.
501
- stream.connect((process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, '') + '/agents', apiKey);
502
- const cwd = resolveWorkingDir(flags, task?.projectId ?? null, log);
503
- if (!cwd) {
504
- exitCode = 1;
505
- return;
506
- }
507
- // Phase D settings were fetched before the lock; the run continues below.
508
- // IMP-560: claim the task so no other agent works it concurrently (the
509
- // server-side lease renews on every mutation and auto-releases here on
510
- // session end). Hard refusals: claimed by someone else, locked to its
511
- // agent assignee, or blocked by the project/task agent-work policy.
512
- try {
513
- const claim = await api.claimTask(taskId);
514
- claimedTaskId = taskId;
515
- log(`[claim] task ${taskId} claimed - lease until ${claim.claimExpiresAt}`);
516
- }
517
- catch (err) {
518
- const e = err;
519
- if (e.code === 'TASK_CLAIMED_BY' ||
520
- e.code === 'AGENT_TASK_LOCKED' ||
521
- e.code === 'AGENT_UNASSIGNED_BLOCKED') {
522
- log(`[claim] cannot work task ${taskId}: ${e.message ?? e.code}`, 'stderr');
523
- exitCode = 1;
524
- return;
525
- }
526
- log(`[claim] warning: could not claim task ${taskId}: ${e.message ?? e.code}`, 'stderr');
527
- }
528
- log(`[teamshare-agent] task ${taskId} as agent ${agentId} (${selfMode ? 'headless' : harness}) in ${cwd}`);
529
- await (0, brief_1.reportStatus)(api, agentId, 'working', taskId, sessionId);
530
- // Keep lastSeenAt fresh during long sessions so the backend liveness
531
- // sweep never mistakes a working agent for a dead one.
532
- heartbeatTimer = setInterval(() => {
533
- void (0, brief_1.reportStatus)(api, agentId, 'working', taskId, sessionId);
534
- }, 60_000);
535
- const brief = await (0, brief_1.fetchTaskBrief)(api, taskId);
536
- const briefPath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `teamshare-task-${taskId}.md`);
537
- (0, node_fs_2.writeFileSync)(briefPath, brief, 'utf8');
538
- log(`[brief] written to ${briefPath} (${brief.length} chars)`);
539
- log('[brief] --- preview ---');
540
- for (const line of brief.split('\n').slice(0, 24))
541
- log(line);
542
- log('[brief] --- end preview ---');
543
- let summary = '';
544
- if (selfMode) {
545
- // BYOK: server-stored provider key > local config > env.
546
- const llmApiKey = sessionLlmApiKey(settings);
547
- if (!llmApiKey) {
548
- log('--self requires a provider API key (set it on the agent in TeamShare, or LLM_API_KEY env)', 'stderr');
549
- exitCode = 1;
550
- return;
551
- }
552
- log('[session] starting headless LLM loop...');
553
- if (flags.get('ask-timeout')) {
554
- process.env.TEAMSHARE_ASK_TIMEOUT = flags.get('ask-timeout') ?? '';
555
- }
556
- const endpoint = llmEndpoint(flags, settings);
557
- log(`[model] ${endpoint.model} @ ${endpoint.baseUrl}`);
558
- const task = await api.getTask(taskId).catch(() => null);
559
- const result = await (0, llm_1.runLlmSession)(api, taskId, brief, {
560
- baseUrl: endpoint.baseUrl,
561
- apiKey: llmApiKey,
562
- model: endpoint.model,
563
- agentName: agentId,
564
- systemPrompt: systemPromptOf(flags, settings),
565
- temperature: temperatureOf(flags, settings),
566
- maxTokens: maxTokensOf(flags, settings),
567
- continue: flags.get('continue') === 'true',
568
- agentUserId: settings.userId ?? undefined,
569
- }, task?.projectId);
570
- summary =
571
- result.summary ||
572
- `Agent run finished after ${result.iterations} iterations (${result.tokensUsed} tokens).`;
573
- log(`[session] done: ${result.finishedBy} | ${result.iterations} iterations | ${result.tokensUsed} tokens`);
574
- }
575
- else {
576
- const systemPrompt = systemPromptOf(flags, settings);
577
- const prompt = `Work on the TeamShare task described in ${briefPath} using the teamshare MCP tools. ` +
578
- `Break the task down first: create 3-8 subtasks with create_subtask, then check each off ` +
579
- `(update_subtask with done: true) as you complete it. ` +
580
- (settings.userId
581
- ? `Your TeamShare user id is ${settings.userId} - when a task or the brief asks you to assign a task to yourself, pass assigneeId "me" (resolves to your user id).\n`
582
- : '') +
583
- (systemPrompt ? `Follow these instructions: ${systemPrompt}\n\n` : '') +
584
- `Keep the human informed with progress comments on the task. When done, post a detailed ` +
585
- `summary comment (mention the subtask progress) and move the task to in_review (or resolved if you are confident).`;
586
- exitCode = await spawnHarness(harness, prompt, log, {
587
- model: modelOf(flags, settings),
588
- temperature: temperatureOf(flags, settings),
589
- cwd,
590
- timeoutMin: sessionTimeoutMin(flags),
591
- title: `TeamShare task ${taskId} (agent ${agentId})`,
592
- agentKey: api.apiKey,
593
- apiBaseUrl: api.baseUrl,
594
- });
595
- summary = `Harness (${harness}) finished the session - see the task comments.`;
596
- }
597
- if (!noReport)
598
- await (0, brief_1.postClosingComment)(api, taskId, summary);
599
- const taskFinal = await (0, brief_1.waitForTaskCompletion)(api, taskId, 15_000, 20 * 60_000, (s) => log(`[status] ${s}`));
600
- log(`[done] task ${taskId} final status: ${taskFinal.status}`);
601
- await (0, brief_1.reportStatus)(api, agentId, 'online', taskId, sessionId);
602
- }
603
- catch (err) {
604
- log(`[error] ${(0, config_1.msg)(err)}`, 'stderr');
605
- exitCode = 1;
606
- }
607
- finally {
608
- if (heartbeatTimer)
609
- clearInterval(heartbeatTimer);
610
- if (claimedTaskId) {
611
- await api.releaseTask(claimedTaskId).catch(() => undefined);
612
- log(`[claim] task ${claimedTaskId} released`);
613
- }
614
- (0, lock_1.releaseLock)(agentId);
615
- await stream.end(exitCode);
616
- }
617
- }
618
- const DONE_STATUSES = new Set(['in_review', 'resolved', 'closed']);
619
- const PRIORITY_RANK = { urgent: 0, high: 1, medium: 2, low: 3 };
620
- /** Document assistant loop: reply to every comment mentioning the agent. */
621
- async function cmdDoc(flags) {
622
- const documentId = need(flags, 'document');
623
- const agentId = need(flags, 'agent');
624
- const apiKey = need(flags, 'key');
625
- const mention = flags.get('mention') ?? '';
626
- const selfMode = flags.get('self') === 'true';
627
- let exitCode = 0;
628
- const sessionId = flags.get('session') ?? (0, session_stream_1.newSessionId)();
629
- const stream = new session_stream_1.SessionStream({ sessionId, agentId, mode: 'chat' });
630
- stream.connect((process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, '') + '/agents', apiKey);
631
- const log = makeLog(stream);
632
- const api = new api_1.TeamshareApi(process.env.TEAMSHARE_API_URL, apiKey);
633
- // PROJECT GUARD (IMP-6xx): hard-block document sessions for documents
634
- // whose project has no linked local folder.
635
- const doc = await api.getDocument(documentId).catch(() => null);
636
- const cwd = resolveWorkingDir(flags, doc?.projectId ?? null, log);
637
- if (!cwd)
638
- return;
639
- const settings = await fetchAgentSettings(api, agentId, log);
640
- const harness = harnessOf(flags, settings);
641
- applyOpenCodeKey(agentId, log, settings);
642
- const lock = await (0, lock_1.acquireLock)(agentId, 'doc', { documentId }, {
643
- onWaiting: (busy) => console.log(`[lock] agent ${agentId} is busy (pid ${busy.pid}, since ${busy.startedAt}, ${busy.kind}) - waiting for it to finish...`),
644
- });
645
- if (!lock) {
646
- console.error(`[lock] gave up waiting for agent ${agentId} - try again later`);
647
- return;
648
- }
649
- try {
650
- await (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
651
- log(`[teamshare-agent] document assistant (document ${documentId}${mention ? `, mention @${mention}` : 'all mentions'}) in ${cwd}`);
652
- const heartbeatTimer = setInterval(() => {
653
- void (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
654
- }, 60_000);
655
- try {
656
- const result = await (0, doc_reply_1.runDocReply)(api, agentId, documentId, mention, {
657
- harness,
658
- selfMode,
659
- echo: process.env.TS_CHAT_ECHO === '1',
660
- api,
661
- llm: {
662
- baseUrl: llmEndpoint(flags, settings).baseUrl,
663
- apiKey: sessionLlmApiKey(settings),
664
- model: llmEndpoint(flags, settings).model,
665
- temperature: temperatureOf(flags, settings),
666
- maxTokens: maxTokensOf(flags, settings),
667
- },
668
- });
669
- log(`[done] replied to ${result.replied} mention${result.replied === 1 ? '' : 's'}` +
670
- (result.skipped ? ` (${result.skipped} skipped)` : ''));
671
- }
672
- finally {
673
- clearInterval(heartbeatTimer);
674
- await (0, brief_1.reportStatus)(api, agentId, 'online', undefined, sessionId);
675
- }
676
- }
677
- catch (err) {
678
- log(`[error] ${(0, config_1.msg)(err)}`, 'stderr');
679
- exitCode = 1;
680
- }
681
- finally {
682
- (0, lock_1.releaseLock)(agentId);
683
- await stream.end(exitCode);
684
- }
685
- }
686
- /** Chat reply loop: reply to a single mentioning message (the triggering one). */
687
- async function cmdChat(flags) {
688
- const projectId = need(flags, 'project');
689
- const agentId = need(flags, 'agent');
690
- const apiKey = need(flags, 'key');
691
- const mention = flags.get('mention') ?? '';
692
- const targetId = flags.get('target');
693
- const selfMode = flags.get('self') === 'true';
694
- let exitCode = 0;
695
- const sessionId = flags.get('session') ?? (0, session_stream_1.newSessionId)();
696
- const stream = new session_stream_1.SessionStream({ sessionId, agentId, projectId, mode: 'chat' });
697
- stream.connect((process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, '') + '/agents', apiKey);
698
- const log = makeLog(stream);
699
- const api = new api_1.TeamshareApi(process.env.TEAMSHARE_API_URL, apiKey);
700
- // PROJECT GUARD (IMP-6xx): hard-block chat sessions for projects without
701
- // a linked local folder.
702
- const cwd = resolveWorkingDir(flags, projectId, log);
703
- if (!cwd)
704
- return;
705
- const settings = await fetchAgentSettings(api, agentId, log);
706
- const harness = harnessOf(flags, settings);
707
- applyOpenCodeKey(agentId, log, settings);
708
- const lock = await (0, lock_1.acquireLock)(agentId, 'chat', { projectId }, {
709
- onWaiting: (busy) => console.log(`[lock] agent ${agentId} is busy (pid ${busy.pid}, since ${busy.startedAt}, ${busy.kind}) - waiting for it to finish...`),
710
- });
711
- if (!lock) {
712
- console.error(`[lock] gave up waiting for agent ${agentId} - try again later`);
713
- return;
714
- }
715
- try {
716
- await (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
717
- log(`[teamshare-agent] chat reply loop (project ${projectId}${mention ? `, mention @${mention}` : 'all mentions'}) in ${cwd}`);
718
- const heartbeatTimer = setInterval(() => {
719
- void (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
720
- }, 60_000);
721
- try {
722
- const result = await (0, chat_reply_1.runChatReply)(api, agentId, projectId, mention, {
723
- harness,
724
- selfMode,
725
- api,
726
- echo: process.env.TS_CHAT_ECHO === '1',
727
- llm: {
728
- baseUrl: (llmEndpoint(flags, settings)).baseUrl,
729
- apiKey: sessionLlmApiKey(settings),
730
- model: (llmEndpoint(flags, settings)).model,
731
- temperature: temperatureOf(flags, settings),
732
- maxTokens: maxTokensOf(flags, settings),
733
- },
734
- // Phase C: stream generated replies into the channel as drafts.
735
- stream: {
736
- wsBase: (process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, ''),
737
- apiKey,
738
- projectId,
739
- },
740
- }, targetId);
741
- log(`[done] replied to ${result.replied} mention${result.replied === 1 ? '' : 's'}` +
742
- (result.skipped ? ` (${result.skipped} skipped)` : ''));
743
- }
744
- finally {
745
- clearInterval(heartbeatTimer);
746
- await (0, brief_1.reportStatus)(api, agentId, 'online', undefined, sessionId);
747
- }
748
- }
749
- catch (err) {
750
- log(`[error] ${(0, config_1.msg)(err)}`, 'stderr');
751
- exitCode = 1;
752
- }
753
- finally {
754
- (0, lock_1.releaseLock)(agentId);
755
- await stream.end(exitCode);
756
- }
757
- }
758
- function buildStatePath(agentId, projectId) {
759
- return (0, node_path_1.join)(BUILD_STATE_DIR, `${agentId}.${projectId}.json`);
760
- }
761
- function loadBuildState(agentId, projectId) {
762
- try {
763
- const parsed = JSON.parse((0, node_fs_1.readFileSync)(buildStatePath(agentId, projectId), 'utf8'));
764
- return {
765
- projectId,
766
- skipped: Array.isArray(parsed.skipped) ? parsed.skipped : [],
767
- done: Number.isFinite(parsed.done) ? parsed.done : 0,
768
- updatedAt: parsed.updatedAt ?? new Date().toISOString(),
769
- };
770
- }
771
- catch {
772
- return { projectId, skipped: [], done: 0, updatedAt: new Date().toISOString() };
773
- }
774
- }
775
- function saveBuildState(agentId, projectId, state) {
776
- try {
777
- (0, node_fs_1.mkdirSync)(BUILD_STATE_DIR, { recursive: true });
778
- (0, node_fs_2.writeFileSync)(buildStatePath(agentId, projectId), JSON.stringify({ ...state, updatedAt: new Date().toISOString() }, null, 2));
779
- }
780
- catch {
781
- /* best-effort - the in-memory state still drives this process */
782
- }
783
- }
784
- function clearBuildState(agentId, projectId) {
785
- try {
786
- (0, node_fs_1.rmSync)(buildStatePath(agentId, projectId), { force: true });
787
- }
788
- catch {
789
- /* best-effort */
790
- }
791
- }
792
- /** Stop file: the daemon writes it on build.stop; the loop honors + deletes it. */
793
- function stopFilePath(agentId) {
794
- return (0, node_path_1.join)(STOP_DIR, `${agentId}.stop`);
795
- }
796
- function writeStopFile(agentId) {
797
- try {
798
- (0, node_fs_1.mkdirSync)(STOP_DIR, { recursive: true });
799
- (0, node_fs_2.writeFileSync)(stopFilePath(agentId), new Date().toISOString(), 'utf8');
800
- }
801
- catch {
802
- /* best-effort */
803
- }
804
- }
805
- function hasStopFile(agentId) {
806
- return (0, node_fs_1.existsSync)(stopFilePath(agentId));
807
- }
808
- function clearStopFile(agentId) {
809
- try {
810
- (0, node_fs_1.rmSync)(stopFilePath(agentId), { force: true });
811
- }
812
- catch {
813
- /* best-effort */
814
- }
815
- }
816
- /** The `build` command: guard + lock + run loop (or --draft / --dry-run). */
817
- async function cmdBuild(flags) {
818
- const agentId = need(flags, 'agent');
819
- const apiKey = need(flags, 'key');
820
- const projectId = need(flags, 'project');
821
- const api = new api_1.TeamshareApi(process.env.TEAMSHARE_API_URL, apiKey);
822
- const log = makeLog(null);
823
- const settings = await fetchAgentSettings(api, agentId, log);
824
- const harness = harnessOf(flags, settings);
825
- let selfMode = flags.get('self') === 'true';
826
- const modelSpec = modelOf(flags, settings);
827
- const modelResolved = (0, models_1.resolveModel)(modelSpec, process.env.LLM_BASE_URL);
828
- if (modelResolved.harnessOnly && selfMode) {
829
- log(`[model] "${modelSpec}" is harness-native - switching from --self to harness mode`);
830
- selfMode = false;
831
- }
832
- // --dry-run: print the queue, work nothing (no lock, no session).
833
- if (flags.get('dry-run') === 'true') {
834
- await cmdBuildDryRun(api, projectId, log);
835
- return;
836
- }
837
- // --draft "<goal>": plan the queue first (creates ready-for-dev tasks).
838
- const draftGoal = flags.get('draft');
839
- if (draftGoal && draftGoal !== 'true') {
840
- log(`[draft] planning the build queue for: ${draftGoal}`);
841
- const planned = await draftBuildPlan({ api, agentId, projectId, settings, flags, log });
842
- if (!planned) {
843
- console.error('[draft] planning failed - see the log above');
844
- process.exitCode = 1;
845
- return;
846
- }
847
- if (flags.get('go') !== 'true') {
848
- console.log('\nPlan created - the tasks are ready-for-dev and ordered.');
849
- console.log('Run again with --go (or start the build from the app) to execute the queue.');
850
- return;
851
- }
852
- }
853
- // Project guard: resolve the working folder before any work.
854
- const stream = new session_stream_1.SessionStream({ sessionId: flags.get('session') ?? (0, session_stream_1.newSessionId)(), agentId, mode: 'build', projectId });
855
- const cwd = resolveWorkingDir(flags, projectId, log);
856
- if (!cwd) {
857
- process.exitCode = 1;
858
- return;
859
- }
860
- // One agent at a time - wait for any in-flight session to finish.
861
- const lock = await (0, lock_1.acquireLock)(agentId, 'build', { projectId }, {
862
- onWaiting: (busy) => log(`[lock] agent ${agentId} is busy (pid ${busy.pid}, since ${busy.startedAt}, ${busy.kind}) - waiting for it to finish...`),
863
- });
864
- if (!lock) {
865
- console.error(`[lock] gave up waiting for agent ${agentId} - try again later`);
866
- process.exitCode = 1;
867
- return;
868
- }
869
- let exitCode = 0;
870
- const sessionId = stream.sessionId;
871
- stream.connect((process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, '') + '/agents', apiKey);
872
- const ctx = { api, agentId, projectId, cwd, selfMode, harness, settings, flags, stream, sessionId, log };
873
- try {
874
- applyOpenCodeKey(agentId, log, settings);
875
- exitCode = await runBuildLoop(ctx);
876
- }
877
- catch (err) {
878
- log(`[build] fatal: ${(0, config_1.msg)(err)}`, 'stderr');
879
- exitCode = 1;
880
- }
881
- finally {
882
- clearStopFile(agentId);
883
- (0, lock_1.releaseLock)(agentId);
884
- await stream.end(exitCode);
885
- }
886
- }
887
- /** Prints the current build queue without working anything (--dry-run). */
888
- async function cmdBuildDryRun(api, projectId, log) {
889
- const queue = await api.getBuildQueue(projectId);
890
- console.log(`Build queue for project ${queue.project.id} (${queue.project.name})`);
891
- console.log(`build agent: ${queue.buildAgent ? queue.buildAgent.name : '(none assigned)'}`);
892
- console.log('');
893
- if (queue.pending.length === 0) {
894
- console.log('pending: none (mark tasks ready-for-dev to add them to the queue)');
895
- }
896
- else {
897
- console.log(`pending (${queue.pending.length}):`);
898
- for (const item of queue.pending) {
899
- const t = item.task;
900
- console.log(` #${item.position} ${t.title} (${t.priority}, ${t.status})`);
901
- }
902
- }
903
- if (queue.blocked.length === 0) {
904
- console.log('blocked: none');
905
- }
906
- else {
907
- console.log(`blocked (${queue.blocked.length}) - waiting for answers:`);
908
- for (const item of queue.blocked) {
909
- console.log(` - ${item.task.title}: ${item.question.body}`);
910
- }
911
- }
912
- }
913
- /** Plans the build queue for a goal: LLM creates ready tasks in order. */
914
- async function draftBuildPlan(ctx) {
915
- const { api, agentId, projectId, settings, flags, log } = ctx;
916
- const llmApiKey = sessionLlmApiKey(settings);
917
- if (!llmApiKey) {
918
- log('--draft requires a provider API key (set it on the agent in TeamShare, or LLM_API_KEY env)', 'stderr');
919
- return '';
920
- }
921
- const endpoint = llmEndpoint(flags, settings);
922
- log(`[draft] model ${endpoint.model} @ ${endpoint.baseUrl}`);
923
- const goal = flags.get('draft') ?? '';
924
- const project = await api.getProject(projectId).catch(() => null);
925
- const prompt = `Plan the work for this goal and create the build queue for it NOW:\n\n` +
926
- `"${goal}"\n\n` +
927
- `Break the goal into 3-8 concrete, independently completable tasks. For each task:\n` +
928
- `- create_task with readyForDev: true and a clear one-line title\n` +
929
- `- set increasing sortOrder (1, 2, 3, ...) so the build works them strictly in order\n` +
930
- `- assign it to yourself with assigneeId "me"\n` +
931
- `- order dependencies first (a task that unblocks others comes before them)\n` +
932
- `After creating them, reply with the ordered task list and the total count.`;
933
- const reply = await (0, llm_1.runProjectSession)(api, projectId, prompt, {
934
- baseUrl: endpoint.baseUrl,
935
- apiKey: llmApiKey,
936
- model: endpoint.model,
937
- agentName: agentId,
938
- systemPrompt: `You are the build planner for project ${project ? `"${project.name}" (${projectId})` : projectId} in TeamShare build mode.\n` +
939
- 'You turn a goal into an ordered, executable build queue. Create each task immediately with create_task ' +
940
- '(readyForDev: true, sortOrder increasing, assigneeId "me"). Do NOT ask for confirmation first - the plan IS the deliverable.',
941
- temperature: 0.2,
942
- agentUserId: settings.userId ?? undefined,
943
- }, project?.name);
944
- log(`[draft] plan reply: ${reply}`);
945
- return reply;
946
- }
947
- /**
948
- * The build run loop: pending -> work -> done; failure -> skip + ask +
949
- * continue; answered questions -> resume; empty queue -> finish. Progress is
950
- * pushed to the backend after every transition. Exits 0 on a clean finish.
951
- */
952
- async function runBuildLoop(ctx) {
953
- const { api, agentId, projectId, cwd, selfMode, harness, settings, flags, stream, sessionId, log } = ctx;
954
- const once = flags.get('once') === 'true';
955
- const onFail = flags.get('on-fail') ?? 'ask';
956
- const blockedWaitMin = Math.max(1, Number(flags.get('blocked-wait')) || 30);
957
- let state = loadBuildState(agentId, projectId);
958
- const queue = await api.getBuildQueue(projectId);
959
- // Crash recovery: a fresh process re-seeds skipped tasks from blocked ones.
960
- if (state.skipped.length === 0 && queue.blocked.length > 0) {
961
- state.skipped = queue.blocked.map((b) => ({
962
- taskId: b.task.id,
963
- title: b.task.title,
964
- askedAt: b.question.askedAt,
965
- }));
966
- saveBuildState(agentId, projectId, state);
967
- }
968
- log(`[build] queue: ${queue.pending.length} pending, ${queue.blocked.length} blocked, ${state.done} done, ${state.skipped.length} waiting`);
969
- const push = (body) => {
970
- const fresh = queue.pending.length + queue.blocked.length;
971
- void api
972
- .pushBuildProgress(agentId, {
973
- projectId,
974
- status: body.status,
975
- total: state.done + fresh,
976
- done: state.done,
977
- remaining: fresh,
978
- currentTaskId: body.currentTaskId,
979
- currentTitle: body.currentTitle,
980
- skippedTaskId: body.skippedTaskId,
981
- })
982
- .catch((err) => log(`[build] progress push failed: ${(0, config_1.msg)(err)}`, 'stderr'));
983
- };
984
- // Live heartbeat: keeps the agent marked working during long sessions.
985
- let currentTaskId;
986
- const heartbeatTimer = setInterval(() => {
987
- void (0, brief_1.reportStatus)(api, agentId, 'working', currentTaskId, sessionId);
988
- }, 60_000);
989
- await (0, brief_1.reportStatus)(api, agentId, 'working', currentTaskId, sessionId);
990
- try {
991
- push({ status: 'running' });
992
- for (;;) {
993
- if (hasStopFile(agentId)) {
994
- clearStopFile(agentId);
995
- log('[build] stop requested - wrapping up');
996
- push({ status: 'stopped' });
997
- return 0;
998
- }
999
- const q = await api.getBuildQueue(projectId);
1000
- // 1) Strict order: work the first pending ready-for-dev task.
1001
- const next = q.pending[0];
1002
- if (next) {
1003
- currentTaskId = next.task.id;
1004
- push({ status: 'running', currentTaskId: next.task.id, currentTitle: next.task.title });
1005
- log(`[build] task ${next.position}/${q.pending.length + q.blocked.length + state.skipped.length}: "${next.task.title}"`);
1006
- const outcome = await workBuildTask(ctx, next.task);
1007
- if (outcome === 'done') {
1008
- state.done += 1;
1009
- saveBuildState(agentId, projectId, state);
1010
- log(`[build] task completed (${state.done} done)`);
1011
- push({ status: 'running' });
1012
- if (once) {
1013
- log('[build] --once: stopping after the first task');
1014
- push({ status: 'finished' });
1015
- return 0;
1016
- }
1017
- continue;
1018
- }
1019
- // Failure: skip + ask (or stop the run).
1020
- if (onFail === 'stop') {
1021
- log(`[build] task "${next.task.title}" failed and --on-fail stop is set - stopping the run`, 'stderr');
1022
- push({ status: 'stopped', skippedTaskId: next.task.id });
1023
- return 0;
1024
- }
1025
- if (state.skipped.some((s) => s.taskId === next.task.id)) {
1026
- log(`[build] task "${next.task.title}" failed again - re-asking`);
1027
- }
1028
- else {
1029
- state.skipped.push({ taskId: next.task.id, title: next.task.title, askedAt: new Date().toISOString() });
1030
- }
1031
- saveBuildState(agentId, projectId, state);
1032
- await askBuildQuestion(api, projectId, next.task, ctx.log);
1033
- push({ status: 'running', skippedTaskId: next.task.id });
1034
- if (once) {
1035
- log('[build] --once: stopping after the first task (skipped)');
1036
- push({ status: 'stopped' });
1037
- return 0;
1038
- }
1039
- continue;
1040
- }
1041
- // 2) No pending work - resume the first skipped task whose question
1042
- // has been answered (open question gone = human answered).
1043
- let resumed = null;
1044
- for (const s of state.skipped) {
1045
- const qState = await api.getTaskQuestions(s.taskId).catch(() => ({ open: null }));
1046
- if (!qState.open) {
1047
- resumed = { taskId: s.taskId, title: s.title };
1048
- break;
1049
- }
1050
- }
1051
- if (resumed) {
1052
- currentTaskId = resumed.taskId;
1053
- log(`[build] resuming "${resumed.title}" (question answered)`);
1054
- const task = await api.getTask(resumed.taskId).catch(() => null);
1055
- if (task) {
1056
- const outcome = await workBuildTask(ctx, task);
1057
- if (outcome === 'done') {
1058
- state.skipped = state.skipped.filter((s) => s.taskId !== resumed.taskId);
1059
- state.done += 1;
1060
- saveBuildState(agentId, projectId, state);
1061
- log(`[build] resumed task completed (${state.done} done)`);
1062
- push({ status: 'running' });
1063
- }
1064
- else if (!(await api.getTaskQuestions(resumed.taskId).catch(() => ({ open: null }))).open) {
1065
- log(`[build] resumed task failed again - re-asking`);
1066
- await askBuildQuestion(api, projectId, task, ctx.log);
1067
- }
1068
- }
1069
- if (once) {
1070
- log('[build] --once: stopping after the resumed task');
1071
- push({ status: 'stopped' });
1072
- return 0;
1073
- }
1074
- continue;
1075
- }
1076
- // 3) Nothing pending and skipped tasks are still waiting on answers.
1077
- if (state.skipped.length > 0) {
1078
- const deadline = Date.now() + blockedWaitMin * 60_000;
1079
- log(`[build] ${state.skipped.length} task(s) blocked on answers - waiting up to ${blockedWaitMin} min`);
1080
- push({ status: 'blocked' });
1081
- while (Date.now() < deadline) {
1082
- if (hasStopFile(agentId)) {
1083
- clearStopFile(agentId);
1084
- log('[build] stop requested while blocked - exiting');
1085
- push({ status: 'stopped' });
1086
- return 0;
1087
- }
1088
- const q2 = await api.getBuildQueue(projectId);
1089
- if (q2.pending.length > 0)
1090
- break; // new ready task - loop and work it
1091
- let answered = false;
1092
- for (const s of state.skipped) {
1093
- const qState = await api.getTaskQuestions(s.taskId).catch(() => ({ open: null }));
1094
- if (!qState.open) {
1095
- answered = true;
1096
- break;
1097
- }
1098
- }
1099
- if (answered)
1100
- break;
1101
- await new Promise((r) => setTimeout(r, 15_000));
1102
- }
1103
- continue; // re-evaluate: pending, answered, or still blocked (loops back)
1104
- }
1105
- // 4) Queue empty and nothing waiting - the run is finished.
1106
- push({ status: 'finished' });
1107
- log(`[build] queue finished - ${state.done} task(s) completed`);
1108
- clearBuildState(agentId, projectId);
1109
- try {
1110
- const channels = await api.listChatChannels(projectId);
1111
- const ch = channels.find((c) => c.name === AGENT_QUESTIONS_CHANNEL);
1112
- const target = ch ?? channels[0];
1113
- if (target) {
1114
- await api.postChatMessage(projectId, `Build run finished: ${state.done} task(s) completed from the ready-for-dev queue.`, target.id);
1115
- }
1116
- }
1117
- catch (err) {
1118
- log(`[build] summary message failed: ${(0, config_1.msg)(err)}`, 'stderr');
1119
- }
1120
- return 0;
1121
- }
1122
- }
1123
- finally {
1124
- clearInterval(heartbeatTimer);
1125
- await (0, brief_1.reportStatus)(api, agentId, 'online', undefined, sessionId);
1126
- }
1127
- }
1128
- /** Works ONE build task to completion; returns 'done' or 'failed'. */
1129
- async function workBuildTask(ctx, task) {
1130
- const { api, agentId, cwd, selfMode, harness, settings, flags, sessionId, log } = ctx;
1131
- const taskId = task.id;
1132
- ctx.stream.meta.taskId = taskId;
1133
- let claimed = false;
1134
- try {
1135
- const claim = await api.claimTask(taskId);
1136
- claimed = true;
1137
- log(`[claim] task ${taskId} claimed - lease until ${claim.claimExpiresAt}`);
1138
- }
1139
- catch (err) {
1140
- const e = err;
1141
- log(`[claim] cannot work task ${taskId}: ${e.message ?? e.code}`, 'stderr');
1142
- return 'failed';
1143
- }
1144
- await (0, brief_1.reportStatus)(api, agentId, 'working', taskId, sessionId);
1145
- try {
1146
- const brief = await (0, brief_1.fetchTaskBrief)(api, taskId);
1147
- const briefPath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), `teamshare-build-${taskId}.md`);
1148
- (0, node_fs_2.writeFileSync)(briefPath, brief, 'utf8');
1149
- log(`[brief] written to ${briefPath} (${brief.length} chars)`);
1150
- log('[brief] --- preview ---');
1151
- for (const line of brief.split('\n').slice(0, 18))
1152
- log(line);
1153
- log('[brief] --- end preview ---');
1154
- let summary = '';
1155
- if (selfMode) {
1156
- const llmApiKey = sessionLlmApiKey(settings);
1157
- if (!llmApiKey) {
1158
- log('--self requires a provider API key (set it on the agent in TeamShare, or LLM_API_KEY env)', 'stderr');
1159
- return 'failed';
1160
- }
1161
- const endpoint = llmEndpoint(flags, settings);
1162
- log(`[model] ${endpoint.model} @ ${endpoint.baseUrl}`);
1163
- const result = await (0, llm_1.runLlmSession)(api, taskId, brief, {
1164
- baseUrl: endpoint.baseUrl,
1165
- apiKey: llmApiKey,
1166
- model: endpoint.model,
1167
- agentName: agentId,
1168
- systemPrompt: systemPromptOf(flags, settings),
1169
- temperature: temperatureOf(flags, settings),
1170
- maxTokens: maxTokensOf(flags, settings),
1171
- continue: flags.get('continue') === 'true',
1172
- agentUserId: settings.userId ?? undefined,
1173
- }, task.projectId);
1174
- summary =
1175
- result.summary ||
1176
- `Agent run finished after ${result.iterations} iterations (${result.tokensUsed} tokens).`;
1177
- log(`[session] done: ${result.finishedBy} | ${result.iterations} iterations | ${result.tokensUsed} tokens`);
1178
- }
1179
- else {
1180
- const systemPrompt = systemPromptOf(flags, settings);
1181
- const prompt = `Work on the TeamShare task described in ${briefPath} using the teamshare MCP tools. ` +
1182
- `This task is part of a BUILD RUN - complete it fully before finishing. ` +
1183
- `Break the task down first: create 3-8 subtasks with create_subtask, then check each off ` +
1184
- `(update_subtask with done: true) as you complete it. ` +
1185
- (settings.userId
1186
- ? `Your TeamShare user id is ${settings.userId} - when a task or the brief asks you to assign a task to yourself, pass assigneeId "me" (resolves to your user id).\n`
1187
- : '') +
1188
- (systemPrompt ? `Follow these instructions: ${systemPrompt}\n\n` : '') +
1189
- `Keep the human informed with progress comments on the task. When done, post a detailed ` +
1190
- `summary comment (mention the subtask progress) and move the task to in_review (or resolved if you are confident).`;
1191
- const exitCode = await spawnHarness(harness, prompt, log, {
1192
- model: modelOf(flags, settings),
1193
- temperature: temperatureOf(flags, settings),
1194
- cwd,
1195
- timeoutMin: sessionTimeoutMin(flags),
1196
- title: `TeamShare build task ${taskId} (agent ${agentId})`,
1197
- agentKey: api.apiKey,
1198
- apiBaseUrl: api.baseUrl,
1199
- });
1200
- summary = exitCode === 0 ? `Harness (${harness}) finished the session - see the task comments.` : `Harness (${harness}) exited with code ${exitCode} - see the task comments.`;
1201
- }
1202
- if (flags.get('no-report') !== 'true')
1203
- await (0, brief_1.postClosingComment)(api, taskId, summary);
1204
- const final = await (0, brief_1.waitForTaskCompletion)(api, taskId, 15_000, 15 * 60_000, (s) => log(`[status] ${s}`));
1205
- log(`[done] task ${taskId} final status: ${final.status}`);
1206
- return DONE_STATUSES.has(final.status) ? 'done' : 'failed';
1207
- }
1208
- catch (err) {
1209
- log(`[build] task ${taskId} errored: ${(0, config_1.msg)(err)}`, 'stderr');
1210
- return 'failed';
1211
- }
1212
- finally {
1213
- if (claimed) {
1214
- await api.releaseTask(taskId).catch(() => undefined);
1215
- log(`[claim] task ${taskId} released`);
1216
- }
1217
- }
1218
- }
1219
- /**
1220
- * Asks the team about a failed build task: a [question] comment on the task
1221
- * plus a message in the project's #agent-questions channel (both surfaces
1222
- * count as answerable per the backend). Skips when a question is already open.
1223
- */
1224
- async function askBuildQuestion(api, projectId, task, log) {
1225
- try {
1226
- const current = await api.getTaskQuestions(task.id);
1227
- if (current.open) {
1228
- log(`[build] question already open for "${task.title}" - not re-asking`);
1229
- return;
1230
- }
1231
- }
1232
- catch {
1233
- /* fall through - ask anyway */
1234
- }
1235
- const body = `[question] Build could not complete task "${task.title}". ` +
1236
- `The session output is in the task comments - check them and reply here (or in #agent-questions) ` +
1237
- `with what the agent should do next. The build resumes this task automatically once you answer.`;
1238
- try {
1239
- await api.addComment({ taskId: task.id, body });
1240
- log(`[build] asked about "${task.title}" (task comment)`);
1241
- }
1242
- catch (err) {
1243
- log(`[build] question comment failed: ${(0, config_1.msg)(err)}`, 'stderr');
1244
- }
1245
- try {
1246
- const channels = await api.listChatChannels(projectId);
1247
- let channelId = channels.find((c) => c.name === AGENT_QUESTIONS_CHANNEL)?.id;
1248
- if (!channelId) {
1249
- const created = await api
1250
- .createChatChannel(projectId, AGENT_QUESTIONS_CHANNEL, 'Agent build questions - answering resumes the build')
1251
- .catch(() => null);
1252
- channelId = created?.id;
1253
- }
1254
- if (channelId) {
1255
- await api.postChatMessage(projectId, `Agent needs input on build task "${task.title}": check the task comments and reply - the build resumes automatically.`, channelId);
1256
- }
1257
- }
1258
- catch (err) {
1259
- log(`[build] #${AGENT_QUESTIONS_CHANNEL} message failed: ${(0, config_1.msg)(err)}`, 'stderr');
1260
- }
1261
- }
1262
- /** Picks the agent's next task from the inbox; returns its id or null. */
1263
- async function pickNextTask(api, agentId, log) {
1264
- const inbox = await api.inbox(agentId);
1265
- const pick = inbox.tasks
1266
- .filter((task) => !DONE_STATUSES.has(String(task.status)))
1267
- .sort((a, b) => {
1268
- const pa = PRIORITY_RANK[String(a.priority)] ?? 9;
1269
- const pb = PRIORITY_RANK[String(b.priority)] ?? 9;
1270
- if (pa !== pb)
1271
- return pa - pb;
1272
- return String(a.createdAt).localeCompare(String(b.createdAt));
1273
- })[0];
1274
- if (!pick)
1275
- return null;
1276
- log(`[pickup] picked task ${String(pick.id)}: "${String(pick.title)}" (${String(pick.priority)}, ${String(pick.status)})`);
1277
- return String(pick.id);
1278
- }
1279
- /**
1280
- * Runs the harness (opencode / claude) with piped stdio so every line is
1281
- * teed to the live console. `model`/`temperature` map to opencode flags
1282
- * (claude gets ANTHROPIC_MODEL env only). Returns the child exit code.
1283
- */
1284
- function spawnHarness(harness, prompt, log, settings = {}) {
1285
- if (harness === 'none') {
1286
- log('[harness] none - brief printed above; work it manually.');
1287
- return Promise.resolve(0);
1288
- }
1289
- let chosen = harness;
1290
- if (chosen === 'auto') {
1291
- if (hasBin('opencode'))
1292
- chosen = 'opencode';
1293
- else if (hasBin('claude'))
1294
- chosen = 'claude';
1295
- else {
1296
- log('[harness] neither opencode nor claude found - brief printed above.');
1297
- return Promise.resolve(0);
1298
- }
1299
- }
1300
- log(`[harness] launching ${chosen}...`);
1301
- const cmd = chosen === 'opencode' ? 'opencode' : 'claude';
1302
- const args = [];
1303
- if (chosen === 'opencode') {
1304
- args.push('run');
1305
- // Non-interactive runs must not stall on permission prompts: without
1306
- // --auto, opencode auto-REJECTS any unapproved tool call (MCP tools
1307
- // like create_subtask, reads outside the working folder) and then sits
1308
- // idle at the prompt forever. The working folder is guard-pinned and
1309
- // the run is kill-switched (--session-timeout), so --auto is safe here.
1310
- args.push('--auto');
1311
- // opencode's --model expects `provider/model` ids; TeamShare catalog
1312
- // ids (`provider:model`) are for the --self loop - only pass ids that
1313
- // opencode itself understands (e.g. opencode-go/deepseek-v4-flash) and
1314
- // never pass flags opencode does not support (--temperature) - both
1315
- // made opencode print its help and exit 1 on Windows.
1316
- if (settings.model && settings.model.includes('/')) {
1317
- args.push('--model', settings.model);
1318
- }
1319
- // Spend attribution: every bridge session records a queryable title so
1320
- // `teamshare-agent usage` can split agent work from the user's windows.
1321
- if (settings.title) {
1322
- args.push('--title', settings.title);
1323
- }
1324
- args.push(prompt);
1325
- }
1326
- else {
1327
- args.push('-p', prompt);
1328
- }
1329
- const env = chosen === 'claude' && settings.model
1330
- ? { ...process.env, ANTHROPIC_MODEL: settings.model }
1331
- : undefined;
1332
- // Pin the opencode teamshare MCP server to THIS agent's key: the user's
1333
- // global opencode config carries one shared key (BridgeAgent), so without
1334
- // this override every agent would act as BridgeAgent inside the harness.
1335
- let mcpOverride = null;
1336
- if (chosen === 'opencode' && settings.agentKey) {
1337
- mcpOverride = (0, harness_1.writeOpencodeMcpConfig)(settings.agentKey, settings.apiBaseUrl ?? process.env.TEAMSHARE_API_URL ?? 'http://localhost:4000/api');
1338
- }
1339
- // Windows: spawn a single fully-quoted command through the shell.
1340
- // `spawn(exe, args, { shell: true })` joins the args WITHOUT quoting, so
1341
- // cmd word-splits the prompt and opencode prints help + exits 1.
1342
- const quote = (a) => /[\s"]/.test(a) ? `"${a.replace(/"/g, '\\"')}"` : a;
1343
- const command = [cmd, ...args.map(quote)].join(' ');
1344
- return new Promise((resolve) => {
1345
- const child = (0, node_child_process_1.spawn)(command, {
1346
- shell: true,
1347
- stdio: ['ignore', 'pipe', 'pipe'],
1348
- env: mcpOverride
1349
- ? { ...(env ?? process.env), OPENCODE_CONFIG: mcpOverride.file }
1350
- : env,
1351
- cwd: settings.cwd,
1352
- });
1353
- // Kill-switch: a runaway harness must never burn usage forever. The
1354
- // cap defaults to 60 minutes and is settable per run (--session-timeout)
1355
- // or globally (TEAMSHARE_SESSION_TIMEOUT_MIN).
1356
- const timeoutMs = Math.max(1, settings.timeoutMin ?? 60) * 60_000;
1357
- const killTimer = setTimeout(() => {
1358
- log(`[harness] ${cmd} killed after ${settings.timeoutMin ?? 60} min (session timeout cap)`, 'stderr');
1359
- child.kill();
1360
- resolve(124);
1361
- }, timeoutMs);
1362
- const tee = (chunk, level) => {
1363
- for (const line of chunk.toString('utf8').split(/\r?\n/)) {
1364
- if (line.trim())
1365
- log(line, level);
1366
- }
1367
- };
1368
- child.stdout?.on('data', (c) => tee(c, 'stdout'));
1369
- child.stderr?.on('data', (c) => tee(c, 'stderr'));
1370
- child.on('error', (err) => {
1371
- clearTimeout(killTimer);
1372
- mcpOverride?.cleanup();
1373
- log(`[harness] ${cmd} failed to start: ${(0, config_1.msg)(err)}`, 'stderr');
1374
- resolve(1);
1375
- });
1376
- child.on('close', (code) => {
1377
- clearTimeout(killTimer);
1378
- mcpOverride?.cleanup();
1379
- if (code !== 0)
1380
- log(`[harness] ${cmd} exited with code ${code ?? 'error'}`, 'stderr');
1381
- resolve(code ?? 1);
1382
- });
1383
- });
1384
- }
1385
- function hasBin(bin) {
1386
- const { spawnSync } = require('node:child_process');
1387
- const res = spawnSync(bin, ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' });
1388
- return res.status === 0;
1389
- }
1390
- async function cmdSkills(action) {
1391
- if (action === 'list') {
1392
- const { available, installed, target } = (0, skills_1.listSkills)();
1393
- console.log(`Skills directory: ${target}`);
1394
- console.log(`Shipped with this bridge: ${available.join(', ') || '(none found)'}`);
1395
- console.log(`Installed: ${installed.join(', ') || '(none - run "teamshare-agent skills install")'}`);
1396
- return;
1397
- }
1398
- if (action === 'install') {
1399
- const { copied, target } = (0, skills_1.installSkills)();
1400
- console.log(`[skills] installed ${copied.join(', ') || '(nothing to copy)'} -> ${target}`);
1401
- return;
1402
- }
1403
- if (action === 'uninstall') {
1404
- const { removed, target } = (0, skills_1.uninstallSkills)();
1405
- console.log(`[skills] removed ${removed.join(', ') || '(nothing installed)'} from ${target}`);
1406
- return;
1407
- }
1408
- console.error('skills requires one of: install | list | uninstall');
1409
- process.exitCode = 2;
1410
- }
1411
- /**
1412
- * `key` - manage the LLM provider key agents use to run their harness.
1413
- * Without --agent the key is the GLOBAL default for every agent; with
1414
- * --agent it overrides that agent only. Keys are never printed in full.
1415
- */
1416
- async function cmdMcp(flags, action) {
1417
- const action2 = action ?? 'status';
1418
- const config = (0, config_1.loadConfig)();
1419
- const apiKey = flags.get('key') ?? config.agents[0]?.apiKey ?? '';
1420
- if (action2 === 'wire') {
1421
- if (!apiKey) {
1422
- console.error('mcp wire requires --key <agentApiKey> or a configured agent');
1423
- process.exitCode = 2;
1424
- return;
1425
- }
1426
- const wired = (0, config_1.wireOpencodeMcp)(apiKey);
1427
- console.log(wired
1428
- ? '[mcp] wired TeamShare MCP into opencode.jsonc — agent now has 21 tools'
1429
- : '[mcp] TeamShare MCP already wired — skipping');
1430
- return;
1431
- }
1432
- if (action2 === 'unwire') {
1433
- const removed = (0, config_1.unwireOpencodeMcp)();
1434
- console.log(removed
1435
- ? '[mcp] removed TeamShare MCP from opencode.jsonc'
1436
- : '[mcp] TeamShare MCP not found in opencode.jsonc — nothing to remove');
1437
- return;
1438
- }
1439
- // status
1440
- const status = (0, config_1.mcpStatus)();
1441
- console.log('TeamShare MCP in opencode:');
1442
- console.log(status.configPath
1443
- ? ` config : ${status.configPath}`
1444
- : ' config : opencode.jsonc not found');
1445
- console.log(status.wired
1446
- ? ` status: WIRED — agent has 21 TeamShare tools (create_task, list_tasks, search, etc.)`
1447
- : ' status: NOT WIRED — agent has no TeamShare tools in opencode sessions');
1448
- if (status.wired) {
1449
- const entry = status.entry;
1450
- console.log(` url : ${entry?.url ?? 'n/a'}`);
1451
- const key = entry?.headers?.['X-Api-Key'] ?? '';
1452
- console.log(` key : ${key ? key.slice(0, 8) + '...' : 'n/a'}`);
1453
- }
1454
- }
1455
- async function cmdKey(flags, action) {
1456
- const agentId = flags.get('agent');
1457
- const chatFlag = flags.get('chat') === 'true';
1458
- const action2 = action ?? 'show';
1459
- if (action2 === 'set') {
1460
- const key = flags.get('key') ?? flags.get('set');
1461
- if (!key) {
1462
- console.error('key set requires --key <providerApiKey> [--agent <agentId>] [--chat]');
1463
- process.exitCode = 2;
1464
- return;
1465
- }
1466
- try {
1467
- if (chatFlag) {
1468
- (0, config_1.setChatKey)(key);
1469
- console.log(`[key] stored chat model key (${key.slice(0, 8)}...) - chat replies use deepseek:deepseek-chat at temp 0.1`);
1470
- }
1471
- else {
1472
- (0, config_1.setOpenCodeKey)(key, agentId ?? undefined);
1473
- console.log(agentId
1474
- ? `[key] stored agent-specific LLM key for agent ${agentId} (${key.slice(0, 8)}...)`
1475
- : `[key] stored global agent LLM key (${key.slice(0, 8)}...) - all agents use it unless overridden`);
1476
- }
1477
- }
1478
- catch (err) {
1479
- console.error(`[key] ${(0, config_1.msg)(err)}`);
1480
- process.exitCode = 1;
1481
- }
1482
- return;
1483
- }
1484
- if (action2 === 'clear') {
1485
- if (chatFlag) {
1486
- (0, config_1.setChatKey)(null);
1487
- console.log('[key] cleared the chat model key (chat falls back to LLM_API_KEY env)');
1488
- }
1489
- else {
1490
- (0, config_1.setOpenCodeKey)(null, agentId ?? undefined);
1491
- console.log(agentId
1492
- ? `[key] cleared agent-specific LLM key for ${agentId} (falls back to the global key or your default credentials)`
1493
- : '[key] cleared the global agent LLM key (agents fall back to your default credentials)');
1494
- }
1495
- return;
1496
- }
1497
- // show
1498
- const config = (0, config_1.loadConfig)();
1499
- const global = config.opencodeKey;
1500
- console.log('LLM provider keys for agent harness runs:');
1501
- console.log(global
1502
- ? ` global : set (${global.slice(0, 8)}...) - all agents unless overridden`
1503
- : ' global : not set - agents fall back to your default credentials');
1504
- console.log(process.env.OPENCODE_API_KEY
1505
- ? ' env : OPENCODE_API_KEY set (wins over config)'
1506
- : ' env : not set');
1507
- for (const agent of config.agents) {
1508
- console.log(agent.opencodeKey
1509
- ? ` ${agent.agentId} : agent-specific (${agent.opencodeKey.slice(0, 8)}...)`
1510
- : ` ${agent.agentId} : using global/default`);
1511
- }
1512
- // chat key
1513
- const chatKeyVal = config.chatKey;
1514
- console.log('\nChat model key (chat replies — deepseek:deepseek-chat at temp 0.1):');
1515
- console.log(chatKeyVal
1516
- ? ` config : set (${chatKeyVal.slice(0, 8)}...)`
1517
- : ' config : not set');
1518
- console.log(process.env.CHAT_LLM_API_KEY
1519
- ? ' env CHAT_LLM_API_KEY : set (wins over config)'
1520
- : process.env.LLM_API_KEY
1521
- ? ' env LLM_API_KEY : set (fallback)'
1522
- : ' env : not set');
1523
- }
1524
74
  async function main() {
1525
- const { flags, positionals } = parseArgs(process.argv.slice(2));
75
+ const { flags, positionals } = (0, args_1.parseArgs)(process.argv.slice(2));
1526
76
  if (flags.has('help') || positionals.length === 0) {
1527
77
  console.log(usage());
1528
78
  return;
@@ -1530,37 +80,36 @@ async function main() {
1530
80
  const command = positionals[0];
1531
81
  try {
1532
82
  if (command === 'connect')
1533
- await cmdConnect(flags);
83
+ await (0, connect_1.cmdConnect)(flags);
1534
84
  else if (command === 'link')
1535
- await cmdLink(flags);
85
+ await (0, link_1.cmdLink)(flags);
1536
86
  else if (command === 'unlink')
1537
- await cmdUnlink(flags);
87
+ await (0, link_1.cmdUnlink)(flags);
1538
88
  else if (command === 'run')
1539
- await cmdRun(flags);
89
+ await (0, run_1.cmdRun)(flags);
1540
90
  else if (command === 'continue') {
1541
- // `continue --task <id>` resumes the previous --self session for it.
1542
91
  if (!flags.get('task')) {
1543
92
  console.error('continue requires --task <taskId>');
1544
93
  process.exitCode = 2;
1545
94
  return;
1546
95
  }
1547
96
  flags.set('continue', 'true');
1548
- await cmdRun(flags);
97
+ await (0, run_1.cmdRun)(flags);
1549
98
  }
1550
99
  else if (command === 'chat')
1551
- await cmdChat(flags);
100
+ await (0, chat_1.cmdChat)(flags);
1552
101
  else if (command === 'doc')
1553
- await cmdDoc(flags);
102
+ await (0, doc_1.cmdDoc)(flags);
1554
103
  else if (command === 'build')
1555
- await cmdBuild(flags);
104
+ await (0, build_1.cmdBuild)(flags);
1556
105
  else if (command === 'usage')
1557
- await cmdUsage(flags);
106
+ await (0, usage_1.cmdUsage)(flags);
1558
107
  else if (command === 'skills')
1559
- await cmdSkills(positionals[1]);
108
+ await (0, skills_1.cmdSkills)(positionals[1]);
1560
109
  else if (command === 'key')
1561
- await cmdKey(flags, positionals[1]);
110
+ await (0, key_1.cmdKey)(flags, positionals[1]);
1562
111
  else if (command === 'mcp')
1563
- await cmdMcp(flags, positionals[1]);
112
+ await (0, key_1.cmdMcp)(flags, positionals[1]);
1564
113
  else {
1565
114
  console.error(`Unknown command: ${command}`);
1566
115
  console.log(usage());