teamshare-bridge 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bridge/daemon.js +239 -8
- package/dist/bridge/daemon.js.map +1 -1
- package/dist/bridge/index.js +10 -0
- package/dist/bridge/index.js.map +1 -1
- package/dist/bridge/local-server.d.ts +6 -1
- package/dist/bridge/local-server.js +24 -0
- package/dist/bridge/local-server.js.map +1 -1
- package/dist/bridge/protocol.js +33 -4
- package/dist/bridge/protocol.js.map +1 -1
- package/dist/bridge/spawn.d.ts +20 -4
- package/dist/bridge/spawn.js +93 -29
- package/dist/bridge/spawn.js.map +1 -1
- package/dist/cli/index.js +1102 -64
- package/dist/cli/index.js.map +1 -1
- package/dist/lib/api.d.ts +203 -5
- package/dist/lib/api.js +144 -3
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/chat-reply.d.ts +22 -2
- package/dist/lib/chat-reply.js +139 -67
- package/dist/lib/chat-reply.js.map +1 -1
- package/dist/lib/config.d.ts +103 -0
- package/dist/lib/config.js +272 -0
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/doc-reply.d.ts +11 -0
- package/dist/lib/doc-reply.js +18 -34
- package/dist/lib/doc-reply.js.map +1 -1
- package/dist/lib/harness.d.ts +38 -0
- package/dist/lib/harness.js +139 -0
- package/dist/lib/harness.js.map +1 -0
- package/dist/lib/llm.d.ts +30 -2
- package/dist/lib/llm.js +284 -30
- package/dist/lib/llm.js.map +1 -1
- package/dist/lib/lock.d.ts +1 -1
- package/dist/lib/models.d.ts +11 -0
- package/dist/lib/models.js +30 -1
- package/dist/lib/models.js.map +1 -1
- package/dist/lib/orchestrator/client.d.ts +27 -0
- package/dist/lib/orchestrator/client.js +135 -0
- package/dist/lib/orchestrator/client.js.map +1 -0
- package/dist/lib/orchestrator/index.d.ts +5 -0
- package/dist/lib/orchestrator/index.js +17 -0
- package/dist/lib/orchestrator/index.js.map +1 -0
- package/dist/lib/orchestrator/runner.d.ts +18 -0
- package/dist/lib/orchestrator/runner.js +177 -0
- package/dist/lib/orchestrator/runner.js.map +1 -0
- package/dist/lib/orchestrator/worktree.d.ts +43 -0
- package/dist/lib/orchestrator/worktree.js +243 -0
- package/dist/lib/orchestrator/worktree.js.map +1 -0
- package/dist/lib/session-stream.d.ts +1 -1
- package/dist/lib/skills.d.ts +21 -0
- package/dist/lib/skills.js +80 -0
- package/dist/lib/skills.js.map +1 -0
- package/dist/lib/usage.d.ts +52 -0
- package/dist/lib/usage.js +69 -0
- package/dist/lib/usage.js.map +1 -0
- package/package.json +3 -1
- package/skills/teamshare-auth-rbac/SKILL.md +79 -0
- package/skills/teamshare-cdd/SKILL.md +145 -0
- package/skills/teamshare-modularity/SKILL.md +92 -0
- package/tools/usage_report.py +223 -0
package/dist/cli/index.js
CHANGED
|
@@ -27,45 +27,128 @@ const chat_reply_1 = require("../lib/chat-reply");
|
|
|
27
27
|
const doc_reply_1 = require("../lib/doc-reply");
|
|
28
28
|
const config_1 = require("../lib/config");
|
|
29
29
|
const lock_1 = require("../lib/lock");
|
|
30
|
+
const usage_1 = require("../lib/usage");
|
|
30
31
|
const session_stream_1 = require("../lib/session-stream");
|
|
31
32
|
const models_1 = require("../lib/models");
|
|
33
|
+
const harness_1 = require("../lib/harness");
|
|
34
|
+
const skills_1 = require("../lib/skills");
|
|
32
35
|
const VERSION = '0.1.0';
|
|
33
36
|
const WS_DEFAULT = 'ws://localhost:4000';
|
|
34
|
-
/**
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
37
84
|
}
|
|
38
85
|
/** Model spec -> { baseUrl, model } via the catalog (env LLM_BASE_URL wins). */
|
|
39
|
-
function llmEndpoint(flags) {
|
|
40
|
-
const spec = modelOf(flags);
|
|
86
|
+
function llmEndpoint(flags, agent) {
|
|
87
|
+
const spec = modelOf(flags, agent);
|
|
41
88
|
const resolved = (0, models_1.resolveModel)(spec, process.env.LLM_BASE_URL);
|
|
42
|
-
if (
|
|
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) {
|
|
43
93
|
console.log(`[model] "${spec}" is not a catalogued provider:model spec - using LLM_BASE_URL (${resolved.baseUrl})`);
|
|
44
94
|
}
|
|
45
|
-
return { baseUrl: resolved.baseUrl, model: resolved.model };
|
|
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)');
|
|
46
119
|
}
|
|
47
|
-
|
|
48
|
-
|
|
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;
|
|
49
130
|
if (raw === undefined)
|
|
50
131
|
return undefined;
|
|
51
132
|
const n = Number(raw);
|
|
52
133
|
return Number.isFinite(n) ? Math.min(2, Math.max(0, n)) : undefined;
|
|
53
134
|
}
|
|
54
|
-
function maxTokensOf(flags) {
|
|
55
|
-
const raw = flags.get('max-tokens') ??
|
|
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;
|
|
56
139
|
if (raw === undefined)
|
|
57
140
|
return undefined;
|
|
58
141
|
const n = Number(raw);
|
|
59
142
|
return Number.isInteger(n) && n > 0 ? n : undefined;
|
|
60
143
|
}
|
|
61
|
-
function systemPromptOf(flags) {
|
|
144
|
+
function systemPromptOf(flags, agent) {
|
|
62
145
|
const inline = flags.get('system-prompt');
|
|
63
146
|
if (inline !== undefined)
|
|
64
147
|
return inline;
|
|
65
148
|
const file = flags.get('system-prompt-file');
|
|
66
149
|
if (file && (0, node_fs_1.existsSync)(file))
|
|
67
150
|
return (0, node_fs_1.readFileSync)(file, 'utf8');
|
|
68
|
-
return null;
|
|
151
|
+
return agent?.systemPrompt ?? null;
|
|
69
152
|
}
|
|
70
153
|
/** Console + live-stream logger for a session. */
|
|
71
154
|
function makeLog(stream) {
|
|
@@ -82,6 +165,15 @@ function usage() {
|
|
|
82
165
|
' teamshare-agent connect --agent <agentId> --key <ts_...>',
|
|
83
166
|
' Test the connection and save the agent key in ~/.teamshare/config.json.',
|
|
84
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).',
|
|
173
|
+
' teamshare-agent link --list',
|
|
174
|
+
' teamshare-agent unlink --project <projectId>',
|
|
175
|
+
' Remove the link - sessions for that project are blocked again.',
|
|
176
|
+
'',
|
|
85
177
|
' teamshare-agent run --task <taskId> --agent <agentId> --key <ts_...> [options]',
|
|
86
178
|
' Fetch the task brief and work it with a harness or headless LLM.',
|
|
87
179
|
' Without --task, picks up the agent\'s next assigned task from its inbox',
|
|
@@ -90,28 +182,71 @@ function usage() {
|
|
|
90
182
|
' teamshare-agent continue --task <taskId> --agent <agentId> --key <ts_...> [--self]',
|
|
91
183
|
' Resume the previous --self session for a task (Phase E continuation).',
|
|
92
184
|
'',
|
|
93
|
-
' teamshare-agent chat --project <projectId> --agent <agentId> --key <ts_...> [--mention <token>]',
|
|
94
|
-
' Reply to
|
|
95
|
-
'
|
|
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.',
|
|
96
189
|
'',
|
|
97
190
|
' teamshare-agent doc --document <documentId> --agent <agentId> --key <ts_...> [--mention <token>]',
|
|
98
191
|
' Document assistant (Phase F): read the document (Word/PPT/Excel/PDF)',
|
|
99
192
|
' and reply to every comment that @mentions the agent in its thread.',
|
|
100
193
|
' Spawned by the daemon on document.mention wakes.',
|
|
101
194
|
'',
|
|
195
|
+
' 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
|
+
' 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
|
+
' 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
|
+
' teamshare-agent key set --key <providerApiKey> [--agent <agentId>]',
|
|
220
|
+
' 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
|
+
'',
|
|
102
226
|
'OPTIONS',
|
|
103
227
|
' --harness auto|opencode|claude|none session driver (default: config or auto)',
|
|
104
228
|
' --self headless LLM loop (needs LLM_API_KEY env)',
|
|
105
229
|
' --no-report skip the closing summary comment',
|
|
106
230
|
' --continue resume the previous --self session for this task (Phase E)',
|
|
107
231
|
' --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)',
|
|
108
234
|
' --model <id> model override (Phase D; env LLM_MODEL)',
|
|
109
235
|
' --temperature <0-2> sampling temperature (Phase D; env LLM_TEMPERATURE)',
|
|
110
236
|
' --max-tokens <n> token budget (Phase D; env LLM_MAX_TOKENS)',
|
|
111
237
|
' --system-prompt <text> system prompt override (Phase D)',
|
|
112
238
|
' --system-prompt-file <path> system prompt from a file (Phase D)',
|
|
113
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)',
|
|
114
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>")',
|
|
115
250
|
' -h, --help this help',
|
|
116
251
|
'',
|
|
117
252
|
'ENV',
|
|
@@ -166,8 +301,34 @@ function need(flags, name) {
|
|
|
166
301
|
}
|
|
167
302
|
return value;
|
|
168
303
|
}
|
|
169
|
-
|
|
170
|
-
|
|
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;
|
|
171
332
|
}
|
|
172
333
|
async function cmdConnect(flags) {
|
|
173
334
|
const agentId = need(flags, 'agent');
|
|
@@ -176,15 +337,112 @@ async function cmdConnect(flags) {
|
|
|
176
337
|
await api.heartbeat(agentId, 'online');
|
|
177
338
|
(0, config_1.upsertAgent)(agentId, apiKey);
|
|
178
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');
|
|
179
345
|
console.log('Ready: teamshare-agent run --task <taskId> --agent ' + agentId + '');
|
|
180
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
|
+
}
|
|
181
429
|
async function cmdRun(flags) {
|
|
182
430
|
const agentId = need(flags, 'agent');
|
|
183
431
|
const apiKey = need(flags, 'key');
|
|
184
|
-
|
|
432
|
+
let selfMode = flags.get('self') === 'true';
|
|
185
433
|
const noReport = flags.get('no-report') === 'true';
|
|
186
|
-
const harness = harnessOf(flags);
|
|
187
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
|
+
}
|
|
188
446
|
// One session per agent at a time - wait for any in-flight session to finish.
|
|
189
447
|
const lock = await (0, lock_1.acquireLock)(agentId, 'task', {
|
|
190
448
|
taskId: flags.get('task') ?? undefined,
|
|
@@ -197,15 +455,24 @@ async function cmdRun(flags) {
|
|
|
197
455
|
}
|
|
198
456
|
let exitCode = 0;
|
|
199
457
|
let heartbeatTimer;
|
|
458
|
+
let claimedTaskId;
|
|
200
459
|
const sessionId = flags.get('session') ?? (0, session_stream_1.newSessionId)();
|
|
201
460
|
const stream = new session_stream_1.SessionStream({ sessionId, agentId, mode: 'run' });
|
|
202
|
-
stream.connect((process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, '') + '/agents', apiKey);
|
|
203
461
|
const log = makeLog(stream);
|
|
462
|
+
applyOpenCodeKey(agentId, log, settings);
|
|
204
463
|
try {
|
|
205
464
|
// Explicit --task wins; otherwise pick up the agent's next assigned task
|
|
206
465
|
// from its inbox (urgent > high > medium > low, then oldest; anything
|
|
207
466
|
// in_review/resolved/closed is skipped).
|
|
208
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
|
+
}
|
|
209
476
|
if (!taskId) {
|
|
210
477
|
const picked = await pickNextTask(api, agentId, log);
|
|
211
478
|
if (!picked) {
|
|
@@ -214,9 +481,51 @@ async function cmdRun(flags) {
|
|
|
214
481
|
}
|
|
215
482
|
taskId = picked;
|
|
216
483
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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}`);
|
|
220
529
|
await (0, brief_1.reportStatus)(api, agentId, 'working', taskId, sessionId);
|
|
221
530
|
// Keep lastSeenAt fresh during long sessions so the backend liveness
|
|
222
531
|
// sweep never mistakes a working agent for a dead one.
|
|
@@ -233,9 +542,10 @@ async function cmdRun(flags) {
|
|
|
233
542
|
log('[brief] --- end preview ---');
|
|
234
543
|
let summary = '';
|
|
235
544
|
if (selfMode) {
|
|
236
|
-
|
|
545
|
+
// BYOK: server-stored provider key > local config > env.
|
|
546
|
+
const llmApiKey = sessionLlmApiKey(settings);
|
|
237
547
|
if (!llmApiKey) {
|
|
238
|
-
log('--self requires
|
|
548
|
+
log('--self requires a provider API key (set it on the agent in TeamShare, or LLM_API_KEY env)', 'stderr');
|
|
239
549
|
exitCode = 1;
|
|
240
550
|
return;
|
|
241
551
|
}
|
|
@@ -243,34 +553,44 @@ async function cmdRun(flags) {
|
|
|
243
553
|
if (flags.get('ask-timeout')) {
|
|
244
554
|
process.env.TEAMSHARE_ASK_TIMEOUT = flags.get('ask-timeout') ?? '';
|
|
245
555
|
}
|
|
246
|
-
const endpoint = llmEndpoint(flags);
|
|
556
|
+
const endpoint = llmEndpoint(flags, settings);
|
|
247
557
|
log(`[model] ${endpoint.model} @ ${endpoint.baseUrl}`);
|
|
558
|
+
const task = await api.getTask(taskId).catch(() => null);
|
|
248
559
|
const result = await (0, llm_1.runLlmSession)(api, taskId, brief, {
|
|
249
560
|
baseUrl: endpoint.baseUrl,
|
|
250
561
|
apiKey: llmApiKey,
|
|
251
562
|
model: endpoint.model,
|
|
252
563
|
agentName: agentId,
|
|
253
|
-
systemPrompt: systemPromptOf(flags),
|
|
254
|
-
temperature: temperatureOf(flags),
|
|
255
|
-
maxTokens: maxTokensOf(flags),
|
|
564
|
+
systemPrompt: systemPromptOf(flags, settings),
|
|
565
|
+
temperature: temperatureOf(flags, settings),
|
|
566
|
+
maxTokens: maxTokensOf(flags, settings),
|
|
256
567
|
continue: flags.get('continue') === 'true',
|
|
257
|
-
|
|
568
|
+
agentUserId: settings.userId ?? undefined,
|
|
569
|
+
}, task?.projectId);
|
|
258
570
|
summary =
|
|
259
571
|
result.summary ||
|
|
260
572
|
`Agent run finished after ${result.iterations} iterations (${result.tokensUsed} tokens).`;
|
|
261
573
|
log(`[session] done: ${result.finishedBy} | ${result.iterations} iterations | ${result.tokensUsed} tokens`);
|
|
262
574
|
}
|
|
263
575
|
else {
|
|
264
|
-
const systemPrompt = systemPromptOf(flags);
|
|
576
|
+
const systemPrompt = systemPromptOf(flags, settings);
|
|
265
577
|
const prompt = `Work on the TeamShare task described in ${briefPath} using the teamshare MCP tools. ` +
|
|
266
578
|
`Break the task down first: create 3-8 subtasks with create_subtask, then check each off ` +
|
|
267
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
|
+
: '') +
|
|
268
583
|
(systemPrompt ? `Follow these instructions: ${systemPrompt}\n\n` : '') +
|
|
269
584
|
`Keep the human informed with progress comments on the task. When done, post a detailed ` +
|
|
270
585
|
`summary comment (mention the subtask progress) and move the task to in_review (or resolved if you are confident).`;
|
|
271
586
|
exitCode = await spawnHarness(harness, prompt, log, {
|
|
272
|
-
model: flags
|
|
273
|
-
temperature: temperatureOf(flags),
|
|
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,
|
|
274
594
|
});
|
|
275
595
|
summary = `Harness (${harness}) finished the session - see the task comments.`;
|
|
276
596
|
}
|
|
@@ -287,6 +607,10 @@ async function cmdRun(flags) {
|
|
|
287
607
|
finally {
|
|
288
608
|
if (heartbeatTimer)
|
|
289
609
|
clearInterval(heartbeatTimer);
|
|
610
|
+
if (claimedTaskId) {
|
|
611
|
+
await api.releaseTask(claimedTaskId).catch(() => undefined);
|
|
612
|
+
log(`[claim] task ${claimedTaskId} released`);
|
|
613
|
+
}
|
|
290
614
|
(0, lock_1.releaseLock)(agentId);
|
|
291
615
|
await stream.end(exitCode);
|
|
292
616
|
}
|
|
@@ -300,13 +624,21 @@ async function cmdDoc(flags) {
|
|
|
300
624
|
const apiKey = need(flags, 'key');
|
|
301
625
|
const mention = flags.get('mention') ?? '';
|
|
302
626
|
const selfMode = flags.get('self') === 'true';
|
|
303
|
-
const harness = harnessOf(flags);
|
|
304
627
|
let exitCode = 0;
|
|
305
628
|
const sessionId = flags.get('session') ?? (0, session_stream_1.newSessionId)();
|
|
306
629
|
const stream = new session_stream_1.SessionStream({ sessionId, agentId, mode: 'chat' });
|
|
307
630
|
stream.connect((process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, '') + '/agents', apiKey);
|
|
308
631
|
const log = makeLog(stream);
|
|
309
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);
|
|
310
642
|
const lock = await (0, lock_1.acquireLock)(agentId, 'doc', { documentId }, {
|
|
311
643
|
onWaiting: (busy) => console.log(`[lock] agent ${agentId} is busy (pid ${busy.pid}, since ${busy.startedAt}, ${busy.kind}) - waiting for it to finish...`),
|
|
312
644
|
});
|
|
@@ -316,7 +648,7 @@ async function cmdDoc(flags) {
|
|
|
316
648
|
}
|
|
317
649
|
try {
|
|
318
650
|
await (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
|
|
319
|
-
log(`[teamshare-agent] document assistant (document ${documentId}${mention ? `, mention @${mention}` : 'all mentions'})`);
|
|
651
|
+
log(`[teamshare-agent] document assistant (document ${documentId}${mention ? `, mention @${mention}` : 'all mentions'}) in ${cwd}`);
|
|
320
652
|
const heartbeatTimer = setInterval(() => {
|
|
321
653
|
void (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
|
|
322
654
|
}, 60_000);
|
|
@@ -325,12 +657,13 @@ async function cmdDoc(flags) {
|
|
|
325
657
|
harness,
|
|
326
658
|
selfMode,
|
|
327
659
|
echo: process.env.TS_CHAT_ECHO === '1',
|
|
660
|
+
api,
|
|
328
661
|
llm: {
|
|
329
|
-
baseUrl: llmEndpoint(flags).baseUrl,
|
|
330
|
-
apiKey:
|
|
331
|
-
model: llmEndpoint(flags).model,
|
|
332
|
-
temperature: temperatureOf(flags),
|
|
333
|
-
maxTokens: maxTokensOf(flags),
|
|
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),
|
|
334
667
|
},
|
|
335
668
|
});
|
|
336
669
|
log(`[done] replied to ${result.replied} mention${result.replied === 1 ? '' : 's'}` +
|
|
@@ -350,20 +683,28 @@ async function cmdDoc(flags) {
|
|
|
350
683
|
await stream.end(exitCode);
|
|
351
684
|
}
|
|
352
685
|
}
|
|
353
|
-
/** Chat reply loop: reply to
|
|
686
|
+
/** Chat reply loop: reply to a single mentioning message (the triggering one). */
|
|
354
687
|
async function cmdChat(flags) {
|
|
355
688
|
const projectId = need(flags, 'project');
|
|
356
689
|
const agentId = need(flags, 'agent');
|
|
357
690
|
const apiKey = need(flags, 'key');
|
|
358
691
|
const mention = flags.get('mention') ?? '';
|
|
692
|
+
const targetId = flags.get('target');
|
|
359
693
|
const selfMode = flags.get('self') === 'true';
|
|
360
|
-
const harness = harnessOf(flags);
|
|
361
694
|
let exitCode = 0;
|
|
362
695
|
const sessionId = flags.get('session') ?? (0, session_stream_1.newSessionId)();
|
|
363
696
|
const stream = new session_stream_1.SessionStream({ sessionId, agentId, projectId, mode: 'chat' });
|
|
364
697
|
stream.connect((process.env.TEAMSHARE_WS_URL ?? WS_DEFAULT).replace(/\/$/, '') + '/agents', apiKey);
|
|
365
698
|
const log = makeLog(stream);
|
|
366
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);
|
|
367
708
|
const lock = await (0, lock_1.acquireLock)(agentId, 'chat', { projectId }, {
|
|
368
709
|
onWaiting: (busy) => console.log(`[lock] agent ${agentId} is busy (pid ${busy.pid}, since ${busy.startedAt}, ${busy.kind}) - waiting for it to finish...`),
|
|
369
710
|
});
|
|
@@ -373,22 +714,22 @@ async function cmdChat(flags) {
|
|
|
373
714
|
}
|
|
374
715
|
try {
|
|
375
716
|
await (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
|
|
376
|
-
log(`[teamshare-agent] chat reply loop (project ${projectId}${mention ? `, mention @${mention}` : 'all mentions'})`);
|
|
717
|
+
log(`[teamshare-agent] chat reply loop (project ${projectId}${mention ? `, mention @${mention}` : 'all mentions'}) in ${cwd}`);
|
|
377
718
|
const heartbeatTimer = setInterval(() => {
|
|
378
719
|
void (0, brief_1.reportStatus)(api, agentId, 'working', undefined, sessionId);
|
|
379
720
|
}, 60_000);
|
|
380
721
|
try {
|
|
381
|
-
const endpoint = llmEndpoint(flags);
|
|
382
722
|
const result = await (0, chat_reply_1.runChatReply)(api, agentId, projectId, mention, {
|
|
383
723
|
harness,
|
|
384
724
|
selfMode,
|
|
725
|
+
api,
|
|
385
726
|
echo: process.env.TS_CHAT_ECHO === '1',
|
|
386
727
|
llm: {
|
|
387
|
-
baseUrl:
|
|
388
|
-
apiKey:
|
|
389
|
-
model:
|
|
390
|
-
temperature: temperatureOf(flags),
|
|
391
|
-
maxTokens: maxTokensOf(flags),
|
|
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),
|
|
392
733
|
},
|
|
393
734
|
// Phase C: stream generated replies into the channel as drafts.
|
|
394
735
|
stream: {
|
|
@@ -396,7 +737,7 @@ async function cmdChat(flags) {
|
|
|
396
737
|
apiKey,
|
|
397
738
|
projectId,
|
|
398
739
|
},
|
|
399
|
-
});
|
|
740
|
+
}, targetId);
|
|
400
741
|
log(`[done] replied to ${result.replied} mention${result.replied === 1 ? '' : 's'}` +
|
|
401
742
|
(result.skipped ? ` (${result.skipped} skipped)` : ''));
|
|
402
743
|
}
|
|
@@ -414,6 +755,510 @@ async function cmdChat(flags) {
|
|
|
414
755
|
await stream.end(exitCode);
|
|
415
756
|
}
|
|
416
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
|
+
}
|
|
417
1262
|
/** Picks the agent's next task from the inbox; returns its id or null. */
|
|
418
1263
|
async function pickNextTask(api, agentId, log) {
|
|
419
1264
|
const inbox = await api.inbox(agentId);
|
|
@@ -454,25 +1299,66 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
454
1299
|
}
|
|
455
1300
|
log(`[harness] launching ${chosen}...`);
|
|
456
1301
|
const cmd = chosen === 'opencode' ? 'opencode' : 'claude';
|
|
457
|
-
const args =
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
+
}
|
|
467
1329
|
const env = chosen === 'claude' && settings.model
|
|
468
1330
|
? { ...process.env, ANTHROPIC_MODEL: settings.model }
|
|
469
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(' ');
|
|
470
1344
|
return new Promise((resolve) => {
|
|
471
|
-
const child = (0, node_child_process_1.spawn)(
|
|
472
|
-
shell:
|
|
1345
|
+
const child = (0, node_child_process_1.spawn)(command, {
|
|
1346
|
+
shell: true,
|
|
473
1347
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
474
|
-
env
|
|
1348
|
+
env: mcpOverride
|
|
1349
|
+
? { ...(env ?? process.env), OPENCODE_CONFIG: mcpOverride.file }
|
|
1350
|
+
: env,
|
|
1351
|
+
cwd: settings.cwd,
|
|
475
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);
|
|
476
1362
|
const tee = (chunk, level) => {
|
|
477
1363
|
for (const line of chunk.toString('utf8').split(/\r?\n/)) {
|
|
478
1364
|
if (line.trim())
|
|
@@ -482,10 +1368,14 @@ function spawnHarness(harness, prompt, log, settings = {}) {
|
|
|
482
1368
|
child.stdout?.on('data', (c) => tee(c, 'stdout'));
|
|
483
1369
|
child.stderr?.on('data', (c) => tee(c, 'stderr'));
|
|
484
1370
|
child.on('error', (err) => {
|
|
1371
|
+
clearTimeout(killTimer);
|
|
1372
|
+
mcpOverride?.cleanup();
|
|
485
1373
|
log(`[harness] ${cmd} failed to start: ${(0, config_1.msg)(err)}`, 'stderr');
|
|
486
1374
|
resolve(1);
|
|
487
1375
|
});
|
|
488
1376
|
child.on('close', (code) => {
|
|
1377
|
+
clearTimeout(killTimer);
|
|
1378
|
+
mcpOverride?.cleanup();
|
|
489
1379
|
if (code !== 0)
|
|
490
1380
|
log(`[harness] ${cmd} exited with code ${code ?? 'error'}`, 'stderr');
|
|
491
1381
|
resolve(code ?? 1);
|
|
@@ -497,6 +1387,140 @@ function hasBin(bin) {
|
|
|
497
1387
|
const res = spawnSync(bin, ['--version'], { stdio: 'ignore', shell: process.platform === 'win32' });
|
|
498
1388
|
return res.status === 0;
|
|
499
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
|
+
}
|
|
500
1524
|
async function main() {
|
|
501
1525
|
const { flags, positionals } = parseArgs(process.argv.slice(2));
|
|
502
1526
|
if (flags.has('help') || positionals.length === 0) {
|
|
@@ -507,6 +1531,10 @@ async function main() {
|
|
|
507
1531
|
try {
|
|
508
1532
|
if (command === 'connect')
|
|
509
1533
|
await cmdConnect(flags);
|
|
1534
|
+
else if (command === 'link')
|
|
1535
|
+
await cmdLink(flags);
|
|
1536
|
+
else if (command === 'unlink')
|
|
1537
|
+
await cmdUnlink(flags);
|
|
510
1538
|
else if (command === 'run')
|
|
511
1539
|
await cmdRun(flags);
|
|
512
1540
|
else if (command === 'continue') {
|
|
@@ -523,6 +1551,16 @@ async function main() {
|
|
|
523
1551
|
await cmdChat(flags);
|
|
524
1552
|
else if (command === 'doc')
|
|
525
1553
|
await cmdDoc(flags);
|
|
1554
|
+
else if (command === 'build')
|
|
1555
|
+
await cmdBuild(flags);
|
|
1556
|
+
else if (command === 'usage')
|
|
1557
|
+
await cmdUsage(flags);
|
|
1558
|
+
else if (command === 'skills')
|
|
1559
|
+
await cmdSkills(positionals[1]);
|
|
1560
|
+
else if (command === 'key')
|
|
1561
|
+
await cmdKey(flags, positionals[1]);
|
|
1562
|
+
else if (command === 'mcp')
|
|
1563
|
+
await cmdMcp(flags, positionals[1]);
|
|
526
1564
|
else {
|
|
527
1565
|
console.error(`Unknown command: ${command}`);
|
|
528
1566
|
console.log(usage());
|