teamwork-os 2.0.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.
package/bin/teamos.js ADDED
@@ -0,0 +1,3389 @@
1
+ #!/usr/bin/env node
2
+
3
+ // packages/cli/dist/args.js
4
+ function parseArgs(argv) {
5
+ const positionals = [];
6
+ const flags = {};
7
+ let passthrough = false;
8
+ for (let i = 0; i < argv.length; i++) {
9
+ const arg = argv[i];
10
+ if (passthrough) {
11
+ positionals.push(arg);
12
+ continue;
13
+ }
14
+ if (arg === "--") {
15
+ passthrough = true;
16
+ continue;
17
+ }
18
+ if (!arg.startsWith("-") || arg === "-") {
19
+ positionals.push(arg);
20
+ continue;
21
+ }
22
+ const body = arg.startsWith("--") ? arg.slice(2) : arg.slice(1);
23
+ const eq = body.indexOf("=");
24
+ if (eq > 0) {
25
+ flags[body.slice(0, eq)] = body.slice(eq + 1);
26
+ continue;
27
+ }
28
+ if (body.startsWith("no-") && body.length > 3) {
29
+ flags[body.slice(3)] = false;
30
+ continue;
31
+ }
32
+ const next = argv[i + 1];
33
+ if (next !== void 0 && (!next.startsWith("-") || next === "-")) {
34
+ flags[body] = next;
35
+ i++;
36
+ } else {
37
+ flags[body] = true;
38
+ }
39
+ }
40
+ return { positionals, flags };
41
+ }
42
+ function stringFlag(args, name) {
43
+ const v = args.flags[name];
44
+ return typeof v === "string" ? v : void 0;
45
+ }
46
+ function boolFlag(args, name) {
47
+ const v = args.flags[name];
48
+ if (v === void 0)
49
+ return false;
50
+ if (typeof v === "boolean")
51
+ return v;
52
+ const lowered = v.trim().toLowerCase();
53
+ return lowered !== "false" && lowered !== "0" && lowered !== "no";
54
+ }
55
+ function intFlag(args, name, opts) {
56
+ const raw = args.flags[name];
57
+ if (raw === void 0)
58
+ return void 0;
59
+ if (typeof raw !== "string" || !/^-?\d+$/.test(raw.trim())) {
60
+ return { error: `--${name} must be an integer (got ${JSON.stringify(raw)})` };
61
+ }
62
+ const n = Number(raw.trim());
63
+ if (n < opts.min || n > opts.max) {
64
+ return { error: `--${name} must be between ${opts.min} and ${opts.max} (got ${n})` };
65
+ }
66
+ return n;
67
+ }
68
+
69
+ // packages/cli/dist/context.js
70
+ import fs2 from "node:fs";
71
+
72
+ // packages/shared/dist/env-file.js
73
+ var KEY_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
74
+ function parseEnvFile(raw) {
75
+ const entries = [];
76
+ for (const rawLine of raw.split("\n")) {
77
+ const line = rawLine.trim();
78
+ if (line.length === 0 || line.startsWith("#"))
79
+ continue;
80
+ const eq = line.indexOf("=");
81
+ if (eq === -1)
82
+ continue;
83
+ const key = line.slice(0, eq).trim();
84
+ if (!KEY_RE.test(key))
85
+ continue;
86
+ entries.push({ key, value: stripQuotes(line.slice(eq + 1).trim()) });
87
+ }
88
+ return entries;
89
+ }
90
+ function stripQuotes(raw) {
91
+ if (raw.length >= 2) {
92
+ if (raw.startsWith("'") && raw.endsWith("'")) {
93
+ return raw.slice(1, -1).replace(/'\\''/g, "'");
94
+ }
95
+ if (raw.startsWith('"') && raw.endsWith('"')) {
96
+ return raw.slice(1, -1);
97
+ }
98
+ }
99
+ return raw;
100
+ }
101
+ function formatEnvLine(key, value) {
102
+ if (!KEY_RE.test(key)) {
103
+ throw new Error(`Invalid env key (must match ${KEY_RE.source}): ${key}`);
104
+ }
105
+ const escaped = value.replace(/'/g, "'\\''");
106
+ return `${key}='${escaped}'`;
107
+ }
108
+ function formatEnvFile(entries) {
109
+ if (entries.length === 0)
110
+ return "";
111
+ return entries.map(({ key, value }) => formatEnvLine(key, value)).join("\n") + "\n";
112
+ }
113
+ function isShellSafe(line) {
114
+ const trimmed = line.trim();
115
+ if (trimmed.length === 0 || trimmed.startsWith("#"))
116
+ return true;
117
+ const eq = trimmed.indexOf("=");
118
+ if (eq === -1)
119
+ return true;
120
+ const key = trimmed.slice(0, eq).trim();
121
+ if (!KEY_RE.test(key))
122
+ return false;
123
+ const value = trimmed.slice(eq + 1);
124
+ if (value.length === 0)
125
+ return true;
126
+ if (value.startsWith("'") && value.endsWith("'") && value.length >= 2)
127
+ return true;
128
+ if (value.startsWith('"') && value.endsWith('"') && value.length >= 2)
129
+ return true;
130
+ return !/[\s'"`$\\|&;<>()*?#!]/.test(value);
131
+ }
132
+ function auditEnvFile(raw) {
133
+ const issues = [];
134
+ const lines = raw.split("\n");
135
+ for (let i = 0; i < lines.length; i++) {
136
+ const line = lines[i];
137
+ if (!isShellSafe(line)) {
138
+ issues.push({
139
+ lineNumber: i + 1,
140
+ line,
141
+ reason: "value is not POSIX-shell-source-able (unquoted whitespace or special chars)"
142
+ });
143
+ }
144
+ }
145
+ return issues;
146
+ }
147
+
148
+ // packages/shared/dist/cron.js
149
+ function parseField(field, min, max) {
150
+ const values = [];
151
+ const inBounds = (v) => v >= min && v <= max;
152
+ const parts = field.split(",");
153
+ for (const part of parts) {
154
+ if (part === "*") {
155
+ for (let i = min; i <= max; i++)
156
+ values.push(i);
157
+ } else if (part.startsWith("*/")) {
158
+ const step = parseInt(part.slice(2), 10);
159
+ if (isNaN(step) || step <= 0)
160
+ return null;
161
+ for (let i = min; i <= max; i += step)
162
+ values.push(i);
163
+ } else if (part.includes("/")) {
164
+ const [rangePart, stepStr] = part.split("/");
165
+ const step = parseInt(stepStr, 10);
166
+ if (isNaN(step) || step <= 0)
167
+ return null;
168
+ if (rangePart === "*") {
169
+ for (let i = min; i <= max; i += step)
170
+ values.push(i);
171
+ } else if (rangePart.includes("-")) {
172
+ const [startStr, endStr] = rangePart.split("-");
173
+ const start = parseInt(startStr, 10);
174
+ const end = parseInt(endStr, 10);
175
+ if (isNaN(start) || isNaN(end))
176
+ return null;
177
+ if (!inBounds(start) || !inBounds(end) || start > end)
178
+ return null;
179
+ for (let i = start; i <= end; i += step)
180
+ values.push(i);
181
+ } else {
182
+ const start = parseInt(rangePart, 10);
183
+ if (isNaN(start) || !inBounds(start))
184
+ return null;
185
+ for (let i = start; i <= max; i += step)
186
+ values.push(i);
187
+ }
188
+ } else if (part.includes("-")) {
189
+ const [startStr, endStr] = part.split("-");
190
+ const start = parseInt(startStr, 10);
191
+ const end = parseInt(endStr, 10);
192
+ if (isNaN(start) || isNaN(end))
193
+ return null;
194
+ if (!inBounds(start) || !inBounds(end) || start > end)
195
+ return null;
196
+ for (let i = start; i <= end; i++)
197
+ values.push(i);
198
+ } else {
199
+ const v = parseInt(part, 10);
200
+ if (isNaN(v) || !inBounds(v))
201
+ return null;
202
+ values.push(v);
203
+ }
204
+ }
205
+ return [...new Set(values)].sort((a, b) => a - b);
206
+ }
207
+ function parseCron(expression) {
208
+ const fields = expression.trim().split(/\s+/);
209
+ if (fields.length !== 5)
210
+ return null;
211
+ const [minuteField, hourField, domField, monthField, dowField] = fields;
212
+ const minutes = parseField(minuteField, 0, 59);
213
+ const hours = parseField(hourField, 0, 23);
214
+ const daysOfMonth = parseField(domField, 1, 31);
215
+ const months = parseField(monthField, 1, 12);
216
+ let daysOfWeek = parseField(dowField, 0, 7);
217
+ if (daysOfWeek) {
218
+ daysOfWeek = [...new Set(daysOfWeek.map((d) => d === 7 ? 0 : d))].sort((a, b) => a - b);
219
+ }
220
+ if (!minutes || !hours || !daysOfMonth || !months || !daysOfWeek)
221
+ return null;
222
+ return { minutes, hours, daysOfMonth, months, daysOfWeek };
223
+ }
224
+ function isValidCronExpression(expression) {
225
+ return parseCron(expression) !== null;
226
+ }
227
+ function isValidTimezone(tz) {
228
+ try {
229
+ new Intl.DateTimeFormat("en-US", { timeZone: tz });
230
+ return true;
231
+ } catch {
232
+ return false;
233
+ }
234
+ }
235
+
236
+ // packages/shared/dist/jobs.js
237
+ var JOB_PRIORITIES = ["critical", "high", "normal", "low"];
238
+ function toJobPriority(value) {
239
+ const s = typeof value === "string" ? value : "";
240
+ return JOB_PRIORITIES.includes(s) ? s : "normal";
241
+ }
242
+
243
+ // packages/shared/dist/model-aliases.js
244
+ var MODEL_ALIASES = ["low", "medium", "high", "elite", "legendary"];
245
+ var DEFAULT_MODEL_ALIASES = {
246
+ legendary: { model: "claude-fable-5", effortLevel: "xhigh" },
247
+ elite: { model: "claude-opus-5", effortLevel: "xhigh" },
248
+ high: { model: "claude-opus-5", effortLevel: "medium" },
249
+ medium: { model: "claude-sonnet-5", effortLevel: "medium" },
250
+ low: { model: "claude-haiku-4-5-20251001" }
251
+ };
252
+ var MODEL_ALIAS_LABELS = {
253
+ legendary: "Legendary",
254
+ elite: "Elite",
255
+ high: "High",
256
+ medium: "Medium",
257
+ low: "Low"
258
+ };
259
+ function formatModelAliasTarget(target) {
260
+ const model = target.model.replace(/^claude-/, "");
261
+ return target.effortLevel ? `${model} @ ${target.effortLevel}` : model;
262
+ }
263
+ var MODEL_ALIAS_OPTIONS = [
264
+ "legendary",
265
+ "elite",
266
+ "high",
267
+ "medium",
268
+ "low"
269
+ ].map((value) => {
270
+ const alias = value;
271
+ return {
272
+ value: alias,
273
+ label: `${MODEL_ALIAS_LABELS[alias]} \xB7 ${formatModelAliasTarget(DEFAULT_MODEL_ALIASES[alias])}`
274
+ };
275
+ });
276
+
277
+ // packages/shared/dist/claude-jsonl/normalize.js
278
+ var MAX_TOOL_RESULT_BYTES = 64 * 1024;
279
+
280
+ // packages/shared/dist/migrate/plan.js
281
+ var TIMEOUT_SEC_MIN = 10;
282
+ var TIMEOUT_SEC_MAX = 6 * 60 * 60;
283
+ var TIMEOUT_SEC_DEFAULT = 900;
284
+ var RETENTION_DAYS_MIN = 1;
285
+ var RETENTION_DAYS_MAX = 3650;
286
+ var RETRY_COUNT_MAX = 3;
287
+ var RETRY_DELAY_MIN = 60;
288
+ var RETRY_DELAY_MAX = 3600;
289
+ var NAME_MAX_LEN = 256;
290
+ var DESCRIPTION_MAX_LEN = 2048;
291
+ var ENV_MAX_ENTRIES = 64;
292
+ var ENV_KEY_RE = /^[A-Za-z_][A-Za-z0-9_]{0,255}$/;
293
+ var PERSONA_MAX_LEN = 64 * 1024;
294
+ var SPEND_CAP_MAX_USD = 1e5;
295
+ var SAFE_ID = /^[\w-]{1,128}$/;
296
+ var REASON_MAX_LEN = 512;
297
+ var MASKED_VALUE = "<hidden>";
298
+ var STATE_KEYS = /* @__PURE__ */ new Map([
299
+ ["lastRun", "run history starts empty in the new home"],
300
+ ["lastStatus", "run history starts empty in the new home"],
301
+ ["lastFailureReason", "run history starts empty in the new home"],
302
+ ["lastSkipReason", "run history starts empty in the new home"],
303
+ ["retryAttempts", "runner-owned counter; resets per cycle"],
304
+ ["consecutiveSkips", "runner-owned counter; resets on first run"],
305
+ ["activeSessionId", "points at a session in the legacy sessions DB"],
306
+ ["pausedReason", "the import sets its own pause reason"]
307
+ ]);
308
+ var DROPPED_JOB_KEYS = /* @__PURE__ */ new Map([
309
+ ["scheduleHuman", "a derived label; the new scheduler renders it from the cron expression"],
310
+ ["tags", "job tags have no equivalent \u2014 the rebuilt Job carries no taxonomy"],
311
+ ["skipIfRunning", "per-job serialization is now unconditional: the scheduler never overlaps two runs of one job"],
312
+ ["claudeAiMcp", "a toggle for one hosted MCP server; the rebuilt Job declares the servers it wants by name in mcpServers, so there is no boolean to carry"],
313
+ [
314
+ "profile",
315
+ "named a legacy engine profile whose settings live outside the job file (and outside --from/jobs/), so there is nothing here to read them from. The rebuilt Job carries its own runtime identity \u2014 set model / persona / pluginDirs / mcpServers on the job by hand"
316
+ ],
317
+ ["dependsOn", "job chaining is not in the rebuild"],
318
+ ["jobTriggers", "job chaining is not in the rebuild"],
319
+ ["outputs", "job chaining is not in the rebuild"],
320
+ ["inputs", "job chaining is not in the rebuild"],
321
+ ["triggers", "webhook triggers are not in the rebuild"],
322
+ [
323
+ "engineOverrides",
324
+ "the rebuilt Job carries model / pluginDirs / mcpServers itself, but this legacy override block is not mapped field by field \u2014 re-enter what you still want on the job"
325
+ ],
326
+ ["remoteHost", "remote execution is not in the rebuild"],
327
+ ["autonomy", "the autonomy ladder is not in the rebuild"],
328
+ ["memory", "the memory subsystem is not in the rebuild"]
329
+ ]);
330
+ function str(v) {
331
+ return typeof v === "string" && v.length > 0 ? v : void 0;
332
+ }
333
+ function isEmptyish(v) {
334
+ if (v === void 0 || v === null || v === "")
335
+ return true;
336
+ if (Array.isArray(v))
337
+ return v.length === 0;
338
+ if (typeof v === "object")
339
+ return Object.keys(v).length === 0;
340
+ return false;
341
+ }
342
+ function summarize(v) {
343
+ if (v === void 0)
344
+ return "undefined";
345
+ if (typeof v === "string")
346
+ return v.length > 60 ? `${JSON.stringify(v.slice(0, 57))}\u2026` : JSON.stringify(v);
347
+ const s = JSON.stringify(v) ?? String(v);
348
+ return s.length > 60 ? `${s.slice(0, 57)}\u2026` : s;
349
+ }
350
+ function describeShape(v) {
351
+ if (v === void 0)
352
+ return "undefined";
353
+ if (v === null)
354
+ return "null";
355
+ if (typeof v === "number" || typeof v === "boolean")
356
+ return String(v);
357
+ if (typeof v === "string")
358
+ return `string, ${v.length} char(s) \u2014 value withheld`;
359
+ if (Array.isArray(v))
360
+ return `array, ${v.length} item(s) \u2014 values withheld`;
361
+ if (typeof v === "object")
362
+ return `object, ${Object.keys(v).length} key(s) \u2014 values withheld`;
363
+ return `${typeof v} \u2014 value withheld`;
364
+ }
365
+ function clampInt(value, min, max) {
366
+ const rounded = Math.trunc(value);
367
+ if (rounded < min)
368
+ return { value: min, clamped: true };
369
+ if (rounded > max)
370
+ return { value: max, clamped: true };
371
+ return { value: rounded, clamped: rounded !== value };
372
+ }
373
+ function placeholderPersona(employee) {
374
+ return `You are the "${employee}" agent, imported from the legacy teamwork-os org. Replace this text with the persona you actually want.`.slice(0, PERSONA_MAX_LEN);
375
+ }
376
+ function mapPreflight(raw, items) {
377
+ if (!Array.isArray(raw)) {
378
+ if (raw !== void 0) {
379
+ items.push({ kind: "dropped", from: "preflight", value: summarize(raw), note: "not an array" });
380
+ }
381
+ return void 0;
382
+ }
383
+ const out = [];
384
+ for (const entry of raw) {
385
+ if (!entry || typeof entry !== "object") {
386
+ items.push({ kind: "dropped", from: "preflight[]", value: summarize(entry), note: "not an object" });
387
+ continue;
388
+ }
389
+ const e = entry;
390
+ const name = str(e.name);
391
+ const url = str(e.url);
392
+ const filePath = str(e.path);
393
+ if (e.kind === "env" && name)
394
+ out.push({ kind: "env", name });
395
+ else if (e.kind === "file" && filePath)
396
+ out.push({ kind: "file", path: filePath });
397
+ else if (e.kind === "http" && url) {
398
+ const timeoutMs = typeof e.timeoutMs === "number" && e.timeoutMs > 0 ? Math.trunc(e.timeoutMs) : void 0;
399
+ out.push(timeoutMs !== void 0 ? { kind: "http", url, timeoutMs } : { kind: "http", url });
400
+ } else {
401
+ items.push({
402
+ kind: "dropped",
403
+ from: "preflight[]",
404
+ value: summarize(entry),
405
+ note: "unsupported check kind \u2014 the rebuild supports env / file / http"
406
+ });
407
+ }
408
+ }
409
+ if (out.length > 0) {
410
+ items.push({ kind: "mapped", from: "preflight", to: "preflight", value: `${out.length} check(s)` });
411
+ }
412
+ return out.length > 0 ? out : void 0;
413
+ }
414
+ function mapEnv(raw, items, warnings) {
415
+ if (raw === void 0 || raw === null)
416
+ return void 0;
417
+ if (typeof raw !== "object" || Array.isArray(raw)) {
418
+ items.push({ kind: "dropped", from: "env", value: describeShape(raw), note: "not an object" });
419
+ return void 0;
420
+ }
421
+ const entries = Object.entries(raw);
422
+ if (entries.length === 0) {
423
+ items.push({ kind: "dropped", from: "env", value: "{}", note: "empty" });
424
+ return void 0;
425
+ }
426
+ const out = {};
427
+ let kept = 0;
428
+ for (const [key, value] of entries) {
429
+ if (typeof value !== "string" || !ENV_KEY_RE.test(key)) {
430
+ items.push({ kind: "dropped", from: `env.${key.slice(0, 64)}`, note: "unusable key or non-string value" });
431
+ continue;
432
+ }
433
+ if (kept >= ENV_MAX_ENTRIES) {
434
+ items.push({ kind: "dropped", from: `env.${key.slice(0, 64)}`, note: `over the ${ENV_MAX_ENTRIES}-entry limit` });
435
+ continue;
436
+ }
437
+ out[key] = value;
438
+ kept++;
439
+ items.push({ kind: "mapped", from: `env.${key}`, to: `env.${key}`, value: MASKED_VALUE });
440
+ }
441
+ if (kept > 0) {
442
+ warnings.push(`carries ${kept} env var(s); values were copied but are never printed \u2014 verify them with \`teamos jobs show\``);
443
+ }
444
+ return kept > 0 ? out : void 0;
445
+ }
446
+ function planJob(source, pauseAll, existingJobIds) {
447
+ const raw = source.raw;
448
+ const items = [];
449
+ const warnings = [];
450
+ const seen = /* @__PURE__ */ new Set();
451
+ const take = (key) => {
452
+ seen.add(key);
453
+ return raw[key];
454
+ };
455
+ const id = str(take("id"));
456
+ if (!id || !SAFE_ID.test(id)) {
457
+ return { rejected: `id ${summarize(raw.id)} is missing or not filename-safe (letters, numbers, hyphens, underscores)` };
458
+ }
459
+ if (existingJobIds.has(id)) {
460
+ warnings.push(`a job named "${id}" already exists in the target home \u2014 this import OVERWRITES it. Re-running the same import looks exactly like this; confirm it is not an unrelated job that happens to share the id.`);
461
+ }
462
+ const prompt = str(take("prompt"));
463
+ const command = str(take("command"));
464
+ if (prompt && command) {
465
+ return { rejected: `${id}: both prompt and command are set \u2014 the rebuilt Job is one or the other, so a human has to choose` };
466
+ }
467
+ if (!prompt && !command) {
468
+ return { rejected: `${id}: neither prompt nor command has content` };
469
+ }
470
+ const kind = prompt ? "prompt" : "shell";
471
+ items.push({
472
+ kind: "mapped",
473
+ from: kind,
474
+ to: kind,
475
+ value: `${kind} job, ${(prompt ?? command ?? "").length} chars`
476
+ });
477
+ const inert = kind === "prompt" ? "command" : "prompt";
478
+ if (raw[inert] !== void 0) {
479
+ items.push({ kind: "dropped", from: inert, value: summarize(raw[inert]), note: "empty in the legacy record; this is a " + kind + " job" });
480
+ }
481
+ const schedule = typeof take("schedule") === "string" ? raw.schedule : "";
482
+ if (!isValidCronExpression(schedule)) {
483
+ return { rejected: `${id}: schedule ${summarize(schedule)} does not parse as 5-field cron` };
484
+ }
485
+ items.push({ kind: "mapped", from: "schedule", to: "schedule", value: schedule, note: "copied verbatim \u2014 not normalized" });
486
+ const timezone = str(take("timezone"));
487
+ if (timezone !== void 0 && !isValidTimezone(timezone)) {
488
+ return { rejected: `${id}: timezone ${summarize(timezone)} is not an IANA zone Intl can resolve` };
489
+ }
490
+ if (timezone) {
491
+ items.push({ kind: "mapped", from: "timezone", to: "timezone", value: timezone });
492
+ } else {
493
+ items.push({
494
+ kind: "defaulted",
495
+ from: "(none)",
496
+ to: "timezone",
497
+ value: "absent",
498
+ note: "legacy schedules were host-local wall clock; an absent timezone means exactly that in the new scheduler"
499
+ });
500
+ }
501
+ const scheduleHuman = str(take("scheduleHuman"));
502
+ const name = (str(take("name")) ?? id).slice(0, NAME_MAX_LEN);
503
+ const description = str(take("description"))?.slice(0, DESCRIPTION_MAX_LEN);
504
+ items.push({ kind: "mapped", from: "name", to: "name", value: name });
505
+ if (description)
506
+ items.push({ kind: "mapped", from: "description", to: "description", value: `${description.length} chars` });
507
+ const legacyEnabled = take("enabled") !== false;
508
+ const enabled = pauseAll ? false : legacyEnabled;
509
+ items.push({
510
+ kind: "mapped",
511
+ from: "enabled",
512
+ to: "enabled",
513
+ value: String(enabled),
514
+ note: pauseAll ? `--pause-all: imported disabled (was ${legacyEnabled})` : void 0
515
+ });
516
+ const rawTimeout = take("timeout");
517
+ let timeoutSec = TIMEOUT_SEC_DEFAULT;
518
+ if (typeof rawTimeout === "number" && Number.isFinite(rawTimeout) && rawTimeout > 0) {
519
+ const { value, clamped } = clampInt(rawTimeout, TIMEOUT_SEC_MIN, TIMEOUT_SEC_MAX);
520
+ timeoutSec = value;
521
+ items.push({
522
+ kind: "mapped",
523
+ from: "timeout",
524
+ to: "timeoutSec",
525
+ value: String(value),
526
+ note: clamped ? `legacy value ${rawTimeout} clamped to the ${TIMEOUT_SEC_MIN}\u2013${TIMEOUT_SEC_MAX}s range` : void 0
527
+ });
528
+ if (clamped) {
529
+ warnings.push(`timeout ${rawTimeout} was out of range and became ${value}s \u2014 confirm that is the budget you want`);
530
+ }
531
+ } else {
532
+ items.push({
533
+ kind: "defaulted",
534
+ from: "timeout",
535
+ to: "timeoutSec",
536
+ value: String(TIMEOUT_SEC_DEFAULT),
537
+ note: rawTimeout === void 0 ? "absent in the legacy record" : `unusable legacy value ${summarize(rawTimeout)}`
538
+ });
539
+ }
540
+ const rawRetention = take("logRetention");
541
+ let retentionDays;
542
+ if (typeof rawRetention === "number" && Number.isFinite(rawRetention) && rawRetention > 0) {
543
+ const { value, clamped } = clampInt(rawRetention, RETENTION_DAYS_MIN, RETENTION_DAYS_MAX);
544
+ retentionDays = value;
545
+ items.push({
546
+ kind: "mapped",
547
+ from: "logRetention",
548
+ to: "retentionDays",
549
+ value: String(value),
550
+ note: clamped ? `legacy value ${rawRetention} clamped` : void 0
551
+ });
552
+ } else if (rawRetention !== void 0) {
553
+ items.push({ kind: "dropped", from: "logRetention", value: summarize(rawRetention), note: "unusable value" });
554
+ }
555
+ const employee = str(take("employee"));
556
+ const legacyProfile = str(take("profile"));
557
+ let persona;
558
+ if (employee) {
559
+ persona = placeholderPersona(employee);
560
+ items.push({
561
+ kind: "mapped",
562
+ from: "employee",
563
+ to: "persona",
564
+ value: employee,
565
+ note: "the org/employee subsystem is not in the rebuild; the name seeds a PLACEHOLDER persona on the job \u2014 it is appended to every turn, so replace it or delete it"
566
+ });
567
+ warnings.push(`legacy employee "${employee}" became a PLACEHOLDER \`persona\` on this job, which is appended to the system prompt on every turn \u2014 replace it with the text you want, or remove the field`);
568
+ } else if (raw.employee !== void 0) {
569
+ items.push({
570
+ kind: "dropped",
571
+ from: "employee",
572
+ value: summarize(raw.employee),
573
+ note: "not a usable employee name, so it seeds no persona"
574
+ });
575
+ }
576
+ if (raw.profile !== void 0) {
577
+ items.push({
578
+ kind: "dropped",
579
+ from: "profile",
580
+ value: summarize(raw.profile),
581
+ note: DROPPED_JOB_KEYS.get("profile")
582
+ });
583
+ if (legacyProfile) {
584
+ warnings.push(`named the legacy engine profile "${legacyProfile}"; its settings are NOT imported (they live outside the job file) \u2014 set model / persona / pluginDirs / mcpServers on this job by hand if the run needs them`);
585
+ }
586
+ }
587
+ const rawSpendCap = take("maxCostUsd");
588
+ let spendCapUsd;
589
+ if (typeof rawSpendCap === "number" && Number.isFinite(rawSpendCap) && rawSpendCap > 0) {
590
+ spendCapUsd = Math.min(rawSpendCap, SPEND_CAP_MAX_USD);
591
+ items.push({
592
+ kind: "mapped",
593
+ from: "maxCostUsd",
594
+ to: "spendCapUsd",
595
+ value: String(spendCapUsd),
596
+ note: spendCapUsd !== rawSpendCap ? `legacy value ${rawSpendCap} capped at ${SPEND_CAP_MAX_USD}` : void 0
597
+ });
598
+ } else if (rawSpendCap !== void 0) {
599
+ items.push({
600
+ kind: "dropped",
601
+ from: "maxCostUsd",
602
+ value: summarize(rawSpendCap),
603
+ note: "unusable value \u2014 a spend cap has to be a positive number of USD, so this job gets none"
604
+ });
605
+ }
606
+ const rawRetryCount = take("retryCount");
607
+ let retryCount;
608
+ if (typeof rawRetryCount === "number" && Number.isFinite(rawRetryCount) && rawRetryCount > 0) {
609
+ const { value, clamped } = clampInt(rawRetryCount, 0, RETRY_COUNT_MAX);
610
+ retryCount = value;
611
+ items.push({ kind: "mapped", from: "retryCount", to: "retryCount", value: String(value), note: clamped ? "clamped to 0\u20133" : void 0 });
612
+ } else if (rawRetryCount !== void 0) {
613
+ items.push({ kind: "mapped", from: "retryCount", to: "retryCount", value: "0", note: "no stall retries" });
614
+ }
615
+ const rawRetryDelay = take("retryDelay");
616
+ let retryDelay;
617
+ if (retryCount !== void 0 && retryCount > 0 && typeof rawRetryDelay === "number" && Number.isFinite(rawRetryDelay)) {
618
+ const { value, clamped } = clampInt(rawRetryDelay, RETRY_DELAY_MIN, RETRY_DELAY_MAX);
619
+ retryDelay = value;
620
+ items.push({ kind: "mapped", from: "retryDelay", to: "retryDelay", value: String(value), note: clamped ? "clamped to 60\u20133600s" : void 0 });
621
+ } else if (rawRetryDelay !== void 0) {
622
+ items.push({
623
+ kind: "dropped",
624
+ from: "retryDelay",
625
+ value: summarize(rawRetryDelay),
626
+ note: "inert without retryCount > 0"
627
+ });
628
+ }
629
+ const rawPriority = take("priority");
630
+ let priority;
631
+ if (rawPriority !== void 0) {
632
+ priority = toJobPriority(rawPriority);
633
+ items.push({
634
+ kind: "mapped",
635
+ from: "priority",
636
+ to: "priority",
637
+ value: priority,
638
+ note: priority === "normal" && rawPriority !== "normal" ? `unrecognized value ${summarize(rawPriority)} \u2192 normal` : void 0
639
+ });
640
+ }
641
+ const selfImprove = take("selfImprove") === true ? true : void 0;
642
+ if (raw.selfImprove !== void 0) {
643
+ items.push({ kind: "mapped", from: "selfImprove", to: "selfImprove", value: String(selfImprove === true) });
644
+ }
645
+ const ignorePauses = take("ignorePauses") === true ? true : void 0;
646
+ if (raw.ignorePauses !== void 0) {
647
+ items.push({ kind: "mapped", from: "ignorePauses", to: "ignorePauses", value: String(ignorePauses === true) });
648
+ }
649
+ const cwd = str(take("cwd"));
650
+ if (cwd)
651
+ items.push({ kind: "mapped", from: "cwd", to: "cwd", value: cwd });
652
+ const preflight = mapPreflight(take("preflight"), items);
653
+ const env = mapEnv(take("env"), items, warnings);
654
+ const createdAt = str(take("createdAt")) ?? (/* @__PURE__ */ new Date(0)).toISOString();
655
+ items.push({ kind: "mapped", from: "createdAt", to: "createdAt", value: createdAt });
656
+ const catchUp = "run-once";
657
+ items.push({
658
+ kind: "defaulted",
659
+ from: "(none)",
660
+ to: "catchUp",
661
+ value: catchUp,
662
+ note: "matches the legacy missed-run behavior (exactly one catch-up)"
663
+ });
664
+ if (raw.skipIfRunning === false) {
665
+ warnings.push("legacy skipIfRunning was false (overlapping runs allowed); the new scheduler never overlaps two runs of one job");
666
+ }
667
+ for (const [key, value] of Object.entries(raw)) {
668
+ if (seen.has(key))
669
+ continue;
670
+ const stateReason = STATE_KEYS.get(key);
671
+ if (stateReason) {
672
+ items.push({ kind: "state", from: key, value: summarize(value), note: stateReason });
673
+ continue;
674
+ }
675
+ const droppedReason = DROPPED_JOB_KEYS.get(key);
676
+ if (droppedReason) {
677
+ items.push({
678
+ kind: "dropped",
679
+ from: key,
680
+ value: summarize(value),
681
+ note: isEmptyish(value) ? `${droppedReason} (was empty)` : droppedReason
682
+ });
683
+ continue;
684
+ }
685
+ items.push({
686
+ kind: "unrecognized",
687
+ from: key,
688
+ value: describeShape(value),
689
+ note: "this importer has no rule for this key \u2014 review it by hand (the value is withheld: an unknown key may hold a credential)"
690
+ });
691
+ }
692
+ if (scheduleHuman) {
693
+ items.push({
694
+ kind: "dropped",
695
+ from: "scheduleHuman",
696
+ value: scheduleHuman,
697
+ note: `${DROPPED_JOB_KEYS.get("scheduleHuman")} \u2014 verify it still describes "${schedule}"`
698
+ });
699
+ }
700
+ const job = {
701
+ id,
702
+ name,
703
+ ...description !== void 0 ? { description } : {},
704
+ schedule,
705
+ ...timezone !== void 0 ? { timezone } : {},
706
+ enabled,
707
+ ...prompt !== void 0 ? { prompt } : {},
708
+ ...command !== void 0 ? { command } : {},
709
+ ...cwd !== void 0 ? { cwd } : {},
710
+ timeoutSec,
711
+ ...persona !== void 0 ? { persona } : {},
712
+ ...spendCapUsd !== void 0 ? { spendCapUsd } : {},
713
+ ...env !== void 0 ? { env } : {},
714
+ ...retentionDays !== void 0 ? { retentionDays } : {},
715
+ catchUp,
716
+ ...priority !== void 0 ? { priority } : {},
717
+ ...selfImprove !== void 0 ? { selfImprove } : {},
718
+ ...retryCount !== void 0 && retryCount > 0 ? { retryCount } : {},
719
+ ...retryDelay !== void 0 ? { retryDelay } : {},
720
+ ...ignorePauses !== void 0 ? { ignorePauses } : {},
721
+ ...preflight !== void 0 ? { preflight } : {},
722
+ createdAt,
723
+ lastRun: null,
724
+ lastStatus: null,
725
+ ...pauseAll ? { pausedReason: "imported paused by `teamos migrate --pause-all` for the parallel run" } : {}
726
+ };
727
+ return {
728
+ plan: {
729
+ id,
730
+ sourceFile: source.file,
731
+ kind,
732
+ job,
733
+ items,
734
+ warnings
735
+ }
736
+ };
737
+ }
738
+ var DROPPED_SETTINGS_SECTIONS = /* @__PURE__ */ new Map([
739
+ ["org", "the org/employee subsystem is not in the rebuild"],
740
+ ["orgDefaults", "the org/employee subsystem is not in the rebuild"],
741
+ ["pipeline", "the pipeline/workflow subsystem is not in the rebuild"],
742
+ ["pipelineFeatures", "the pipeline/workflow subsystem is not in the rebuild"],
743
+ ["appearance", "the lean dashboard has one theme; no appearance settings"],
744
+ ["schedulerSettings", "the plan-tier / run-budget optimizer is not in the rebuild"],
745
+ ["ollama", "the Ollama engine is not in the rebuild (Claude CLI only)"],
746
+ ["memorySteward", "the memory subsystem is not in the rebuild"],
747
+ ["continuousImprovement", "the autonomy subsystem is not in the rebuild"],
748
+ ["supervisor", "the autonomy subsystem is not in the rebuild"],
749
+ ["heartbeat", "the autonomy subsystem is not in the rebuild"],
750
+ ["prAutoFix", "the autonomy subsystem is not in the rebuild"],
751
+ ["compilePages", "the memory subsystem is not in the rebuild"],
752
+ ["autoEnrich", "the org/enrichment subsystem is not in the rebuild"],
753
+ ["autoEnrichIntervalMinutes", "the org/enrichment subsystem is not in the rebuild"],
754
+ ["autoTuneTimeouts", "timeout auto-tuning is not in the rebuild"],
755
+ ["syncDir", "the sync-directory feature is not in the rebuild"]
756
+ ]);
757
+ var DROPPED_ENGINE_KEYS = /* @__PURE__ */ new Map([
758
+ ["default", "the Claude CLI is the only engine"],
759
+ ["defaultEffortLevel", "no effort-level knob in the rebuilt engine"],
760
+ ["allowOverage", "no overage policy in the rebuilt engine"],
761
+ ["pool", "the session pool is not in the rebuild"],
762
+ ["ollama", "the Ollama engine is not in the rebuild"]
763
+ ]);
764
+ var DROPPED_SLACK_KEYS = /* @__PURE__ */ new Map([
765
+ ["botMcpName", "the connector talks to Slack directly; no bot-MCP indirection"],
766
+ ["threadResume", "a thread is one session by construction now \u2014 always resumed, no toggle"],
767
+ ["threadResumeMaxTurns", "no per-thread turn ceiling in the rebuilt connector"],
768
+ ["threadResumeIdleHours", "no idle expiry in the rebuilt connector"],
769
+ ["streamReplies", "replies never stream; the progress card carries status and the sealed answer"],
770
+ ["heartbeat", "the progress card replaces the heartbeat message"],
771
+ ["autoCompactEnabled", "no auto-compaction in the rebuilt connector"],
772
+ ["autoCompactThresholdTokens", "no auto-compaction in the rebuilt connector"],
773
+ ["responderEmployee", "the org/employee subsystem is not in the rebuild"]
774
+ ]);
775
+ var DROPPED_MAINTENANCE_KEYS = /* @__PURE__ */ new Map([
776
+ ["enabled", "the retention sweep always runs"],
777
+ ["safetyIntervalSecs", "no safety sweep in the rebuild"],
778
+ ["maintenanceIntervalSecs", "the retention sweep runs daily, not on an interval knob"],
779
+ ["diskWarningThresholdMb", "no disk-threshold alerting in the rebuild"],
780
+ ["dbWarningThresholdMb", "no db-threshold alerting in the rebuild"]
781
+ ]);
782
+ function planSettings(legacy, current, now) {
783
+ const items = [];
784
+ const warnings = [];
785
+ const pauseEntries = [];
786
+ if (!legacy) {
787
+ return {
788
+ plan: { settings: current, items: [{ kind: "defaulted", from: "(no legacy settings.json)", to: "settings", value: "target settings unchanged" }], warnings },
789
+ pauseEntries
790
+ };
791
+ }
792
+ const next = {
793
+ server: current.server,
794
+ retention: { ...current.retention },
795
+ engine: { ...current.engine },
796
+ channels: { directory: { ...current.channels.directory } },
797
+ scheduler: { ...current.scheduler },
798
+ slack: { ...current.slack }
799
+ };
800
+ const seen = /* @__PURE__ */ new Set();
801
+ const take = (key) => {
802
+ seen.add(key);
803
+ return legacy[key];
804
+ };
805
+ const mcj = take("maxConcurrentJobs");
806
+ if (typeof mcj === "number" && Number.isFinite(mcj) && mcj > 0) {
807
+ const { value, clamped } = clampInt(mcj, 1, 32);
808
+ next.scheduler.maxConcurrentJobs = value;
809
+ items.push({ kind: "mapped", from: "maxConcurrentJobs", to: "scheduler.maxConcurrentJobs", value: String(value), note: clamped ? "clamped to 1\u201332" : void 0 });
810
+ } else if (mcj !== void 0) {
811
+ items.push({ kind: "dropped", from: "maxConcurrentJobs", value: summarize(mcj), note: "unusable value" });
812
+ }
813
+ const engine = take("engine");
814
+ if (engine && typeof engine === "object" && !Array.isArray(engine)) {
815
+ for (const [key, value] of Object.entries(engine)) {
816
+ const path11 = `engine.${key}`;
817
+ if (key === "defaultModel" && typeof value === "string") {
818
+ next.engine.defaultModel = value;
819
+ items.push({ kind: "mapped", from: path11, to: "engine.defaultModel", value: value === "" ? "(CLI default)" : value });
820
+ } else if (key === "maxRetries" && typeof value === "number" && Number.isFinite(value) && value >= 0) {
821
+ next.engine.maxRetries = clampInt(value, 0, 10).value;
822
+ items.push({ kind: "mapped", from: path11, to: "engine.maxRetries", value: String(next.engine.maxRetries) });
823
+ } else if (key === "retryBaseDelayMs" && typeof value === "number" && Number.isFinite(value) && value > 0) {
824
+ next.engine.retryBaseDelayMs = clampInt(value, 1, 6e4).value;
825
+ items.push({ kind: "mapped", from: path11, to: "engine.retryBaseDelayMs", value: String(next.engine.retryBaseDelayMs) });
826
+ } else if (key === "noProgressTimeoutMs" && typeof value === "number" && Number.isFinite(value) && value >= 0) {
827
+ next.engine.noProgressTimeoutMs = clampInt(value, 0, 6 * 60 * 60 * 1e3).value;
828
+ items.push({ kind: "mapped", from: path11, to: "engine.noProgressTimeoutMs", value: String(next.engine.noProgressTimeoutMs) });
829
+ } else {
830
+ const reason = DROPPED_ENGINE_KEYS.get(key);
831
+ items.push(reason ? { kind: "dropped", from: path11, value: summarize(value), note: reason } : { kind: "unrecognized", from: path11, value: describeShape(value), note: "no rule for this key \u2014 review by hand (value withheld)" });
832
+ }
833
+ }
834
+ }
835
+ const maintenance = take("maintenance");
836
+ if (maintenance && typeof maintenance === "object" && !Array.isArray(maintenance)) {
837
+ for (const [key, value] of Object.entries(maintenance)) {
838
+ const path11 = `maintenance.${key}`;
839
+ if (key === "sessionRetentionDays" && typeof value === "number" && Number.isFinite(value) && value > 0) {
840
+ const { value: days, clamped } = clampInt(value, RETENTION_DAYS_MIN, RETENTION_DAYS_MAX);
841
+ next.retention.days = days;
842
+ items.push({ kind: "mapped", from: path11, to: "retention.days", value: String(days), note: clamped ? "clamped" : void 0 });
843
+ } else {
844
+ const reason = DROPPED_MAINTENANCE_KEYS.get(key);
845
+ items.push(reason ? { kind: "dropped", from: path11, value: summarize(value), note: reason } : { kind: "unrecognized", from: path11, value: describeShape(value), note: "no rule for this key \u2014 review by hand (value withheld)" });
846
+ }
847
+ }
848
+ }
849
+ const slack = take("slack");
850
+ if (slack && typeof slack === "object" && !Array.isArray(slack)) {
851
+ for (const [key, value] of Object.entries(slack)) {
852
+ const path11 = `slack.${key}`;
853
+ if (key === "channelCwd" && value && typeof value === "object" && !Array.isArray(value)) {
854
+ let mapped = 0;
855
+ for (const [channel, cwd] of Object.entries(value)) {
856
+ if (typeof cwd !== "string" || cwd.length === 0 || channel.length === 0) {
857
+ items.push({ kind: "dropped", from: `${path11}.${channel.slice(0, 64)}`, note: "unusable channel or path" });
858
+ continue;
859
+ }
860
+ next.channels.directory[channel] = cwd;
861
+ mapped++;
862
+ items.push({ kind: "mapped", from: `${path11}.${channel}`, to: `channels.directory.${channel}`, value: cwd });
863
+ }
864
+ if (mapped > 0) {
865
+ warnings.push(`${mapped} channel\u2192cwd mapping(s) landed in channels.directory \u2014 every path must exist on this host or the connector falls back to the teamwork home`);
866
+ }
867
+ } else if (key === "requireMention" && typeof value === "boolean") {
868
+ next.slack.requireMention = value;
869
+ items.push({ kind: "mapped", from: path11, to: "slack.requireMention", value: String(value) });
870
+ } else if (key === "ackReaction" && typeof value === "string") {
871
+ next.slack.ackEmoji = value;
872
+ items.push({ kind: "mapped", from: path11, to: "slack.ackEmoji", value: value === "" ? "(ack disabled)" : value });
873
+ } else if (key === "includeChannelHistory" && typeof value === "boolean") {
874
+ next.slack.historyLimit = value ? current.slack.historyLimit : 0;
875
+ items.push({
876
+ kind: "mapped",
877
+ from: path11,
878
+ to: "slack.historyLimit",
879
+ value: String(next.slack.historyLimit),
880
+ note: value ? "history priming stays on; the rebuild expresses it as a message count" : "false \u2192 0 messages of history"
881
+ });
882
+ } else {
883
+ const reason = DROPPED_SLACK_KEYS.get(key);
884
+ items.push(reason ? { kind: "dropped", from: path11, value: summarize(value), note: reason } : { kind: "unrecognized", from: path11, value: describeShape(value), note: "no rule for this key \u2014 review by hand (value withheld)" });
885
+ }
886
+ }
887
+ }
888
+ const globalPause = take("globalPause");
889
+ if (globalPause && typeof globalPause === "object" && !Array.isArray(globalPause)) {
890
+ for (const [key, value] of Object.entries(globalPause)) {
891
+ const path11 = `globalPause.${key}`;
892
+ const section = value && typeof value === "object" && !Array.isArray(value) ? value : null;
893
+ if (key === "auth" && section) {
894
+ if (section.enabled === true) {
895
+ const reason = (str(section.source) ?? "auth pause carried over from the legacy home").slice(0, REASON_MAX_LEN);
896
+ pauseEntries.push({ source: "auth", reason, since: str(section.detectedAt) ?? now });
897
+ items.push({ kind: "mapped", from: path11, to: "scheduler pause (auth)", value: reason });
898
+ warnings.push("the legacy home was auth-paused; the new home imports that pause. NO CLI command clears it \u2014 the gateway's OAuth probe (cron/auth-pause.ts, which re-reads the Claude Code credential every minute while paused) clears it on its own once the credential reads healthy. `teamos jobs resume <id>` un-pauses one JOB and does not touch this; the scheduler-level resume route clears only the manual pause, deliberately, so an operator cannot mask an expired credential.");
899
+ } else {
900
+ items.push({ kind: "mapped", from: path11, to: "scheduler pause (auth)", value: "not paused", note: "nothing to carry" });
901
+ }
902
+ } else if (key === "manual" && section) {
903
+ if (section.enabled === true) {
904
+ const reason = (str(section.reason) ?? "manual pause carried over from the legacy home").slice(0, REASON_MAX_LEN);
905
+ pauseEntries.push({ source: "manual", reason, since: str(section.since) ?? now });
906
+ items.push({ kind: "mapped", from: path11, to: "scheduler pause (manual)", value: reason });
907
+ } else {
908
+ items.push({ kind: "mapped", from: path11, to: "scheduler pause (manual)", value: "not paused", note: "nothing to carry" });
909
+ }
910
+ } else if (key === "schedules") {
911
+ items.push({
912
+ kind: "dropped",
913
+ from: path11,
914
+ value: Array.isArray(value) ? `${value.length} window(s)` : summarize(value),
915
+ note: "scheduled pause windows are not in the rebuild \u2014 the surviving pause sources are manual and auth"
916
+ });
917
+ if (Array.isArray(value) && value.length > 0) {
918
+ warnings.push(`${value.length} legacy pause window(s) (e.g. quiet hours) are NOT carried over \u2014 jobs will fire during them`);
919
+ }
920
+ } else {
921
+ items.push({ kind: "unrecognized", from: path11, value: describeShape(value), note: "no rule for this key \u2014 review by hand (value withheld)" });
922
+ }
923
+ }
924
+ }
925
+ for (const [key, value] of Object.entries(legacy)) {
926
+ if (seen.has(key))
927
+ continue;
928
+ const reason = DROPPED_SETTINGS_SECTIONS.get(key);
929
+ items.push(reason ? { kind: "dropped", from: key, value: summarize(value), note: reason } : { kind: "unrecognized", from: key, value: describeShape(value), note: "no rule for this key \u2014 review by hand (value withheld)" });
930
+ }
931
+ return { plan: { settings: next, items, warnings }, pauseEntries };
932
+ }
933
+ function planMigration(inputs) {
934
+ const existingJobIds = new Set(inputs.target.existingJobIds);
935
+ const jobs = [];
936
+ const rejected = [...inputs.unreadable ?? []].sort((a, b) => a.file.localeCompare(b.file));
937
+ const sorted = [...inputs.jobs].sort((a, b) => a.file.localeCompare(b.file));
938
+ for (const source of sorted) {
939
+ const result = planJob(source, inputs.pauseAll, existingJobIds);
940
+ if ("rejected" in result)
941
+ rejected.push({ file: source.file, reason: result.rejected });
942
+ else
943
+ jobs.push(result.plan);
944
+ }
945
+ const { plan: settings, pauseEntries } = planSettings(inputs.settings, inputs.target.settings, inputs.now);
946
+ return {
947
+ jobs,
948
+ settings,
949
+ pauseEntries,
950
+ rejected,
951
+ pauseAll: inputs.pauseAll,
952
+ now: inputs.now
953
+ };
954
+ }
955
+
956
+ // packages/shared/dist/migrate/render.js
957
+ var KIND_LABEL = {
958
+ mapped: "mapped",
959
+ defaulted: "default",
960
+ state: "not imported",
961
+ dropped: "DROPPED",
962
+ unrecognized: "UNKNOWN KEY"
963
+ };
964
+ function itemLine(item) {
965
+ const arrow = item.to ? ` \u2192 ${item.to}` : "";
966
+ const value = item.value !== void 0 ? ` = ${item.value}` : "";
967
+ const note = item.note ? ` (${item.note})` : "";
968
+ return `${KIND_LABEL[item.kind].padEnd(12)} ${item.from}${arrow}${value}${note}`;
969
+ }
970
+ function byKind(items, kinds) {
971
+ return items.filter((i) => kinds.includes(i.kind));
972
+ }
973
+ function foldedIdentity(job) {
974
+ const out = [];
975
+ if (job.job.persona !== void 0)
976
+ out.push("persona (PLACEHOLDER \u2014 replace it)");
977
+ if (job.job.spendCapUsd !== void 0)
978
+ out.push(`spendCapUsd=${job.job.spendCapUsd}`);
979
+ return out;
980
+ }
981
+ function counts(plan) {
982
+ const all = [...plan.jobs.flatMap((j) => j.items), ...plan.settings.items];
983
+ const out = { mapped: 0, defaulted: 0, state: 0, dropped: 0, unrecognized: 0 };
984
+ for (const item of all)
985
+ out[item.kind]++;
986
+ return out;
987
+ }
988
+ function renderPlan(plan, opts) {
989
+ const lines = [];
990
+ const n = counts(plan);
991
+ lines.push("teamos migrate \u2014 DRY RUN (nothing has been written)");
992
+ lines.push(` from: ${opts.from}`);
993
+ lines.push(` to: ${opts.to}`);
994
+ lines.push(` ${plan.jobs.length} job(s), ${plan.rejected.length} rejected` + (plan.pauseAll ? " [--pause-all: every job imported disabled]" : ""));
995
+ lines.push("");
996
+ for (const job of plan.jobs) {
997
+ const identity = foldedIdentity(job);
998
+ lines.push(`\u2500\u2500 job ${job.id} (${job.sourceFile}, ${job.kind} job)`);
999
+ lines.push(` schedule "${job.job.schedule}"${job.job.timezone ? ` in ${job.job.timezone}` : " (host local)"} \u2014 carried verbatim`);
1000
+ lines.push(` enabled=${job.job.enabled} timeoutSec=${job.job.timeoutSec} runtime: ${identity.length > 0 ? identity.join(", ") : "engine defaults"}`);
1001
+ for (const item of job.items)
1002
+ lines.push(` ${itemLine(item)}`);
1003
+ for (const warning of job.warnings)
1004
+ lines.push(` ! ${warning}`);
1005
+ lines.push("");
1006
+ }
1007
+ if (plan.rejected.length > 0) {
1008
+ lines.push("\u2500\u2500 rejected (NOT imported)");
1009
+ for (const r of plan.rejected)
1010
+ lines.push(` ${r.file}: ${r.reason}`);
1011
+ lines.push("");
1012
+ }
1013
+ lines.push("\u2500\u2500 settings");
1014
+ for (const item of plan.settings.items)
1015
+ lines.push(` ${itemLine(item)}`);
1016
+ for (const warning of plan.settings.warnings)
1017
+ lines.push(` ! ${warning}`);
1018
+ lines.push("");
1019
+ if (plan.pauseEntries.length > 0) {
1020
+ lines.push("\u2500\u2500 scheduler pause state");
1021
+ for (const e of plan.pauseEntries)
1022
+ lines.push(` ${e.source}: ${e.reason} (since ${e.since})`);
1023
+ lines.push("");
1024
+ }
1025
+ lines.push(`totals: ${n.mapped} mapped, ${n.defaulted} defaulted, ${n.state} runtime-state skipped, ${n.dropped} dropped, ${n.unrecognized} unrecognized`);
1026
+ lines.push("Run again with --apply to write it.");
1027
+ return lines.join("\n");
1028
+ }
1029
+ function mdTable(items, columns) {
1030
+ if (items.length === 0)
1031
+ return ["_none_", ""];
1032
+ const rows = [`| ${columns.join(" | ")} |`, "|---|---|---|"];
1033
+ for (const item of items) {
1034
+ const target = item.to ?? "\u2014";
1035
+ const detail = [item.value, item.note].filter(Boolean).join(" \u2014 ");
1036
+ rows.push(`| \`${item.from}\` | ${target === "\u2014" ? "\u2014" : `\`${target}\``} | ${detail.replace(/\|/g, "\\|")} |`);
1037
+ }
1038
+ rows.push("");
1039
+ return rows;
1040
+ }
1041
+ function renderReport(plan, opts) {
1042
+ const n = counts(plan);
1043
+ const lines = [];
1044
+ lines.push("# teamwork-os migration report");
1045
+ lines.push("");
1046
+ lines.push(`- generated: ${plan.now}`);
1047
+ lines.push(`- source home: \`${opts.from}\``);
1048
+ lines.push(`- target home: \`${opts.to}\``);
1049
+ lines.push(`- flags: ${[plan.pauseAll ? "--pause-all" : "", opts.force ? "--force" : ""].filter(Boolean).join(" ") || "(none)"}`);
1050
+ lines.push(`- totals: ${n.mapped} mapped, ${n.defaulted} defaulted, ${n.state} runtime-state skipped, ${n.dropped} dropped, ${n.unrecognized} unrecognized`);
1051
+ if (opts.applied.noop) {
1052
+ lines.push("- **no changes**: every target file already matched this plan.");
1053
+ }
1054
+ lines.push("");
1055
+ lines.push("## Files");
1056
+ lines.push("");
1057
+ for (const f of opts.applied.files)
1058
+ lines.push(`- \`${f.path}\` \u2014 ${f.outcome}`);
1059
+ lines.push("");
1060
+ lines.push("## Jobs");
1061
+ lines.push("");
1062
+ for (const job of plan.jobs) {
1063
+ lines.push(`### \`${job.id}\` (${job.kind} job)`);
1064
+ lines.push("");
1065
+ lines.push(`Source: \`${job.sourceFile}\`. Schedule \`${job.job.schedule}\`${job.job.timezone ? ` in \`${job.job.timezone}\`` : " (host-local wall clock)"} \u2014 carried across verbatim, validated with the scheduler's own parser.`);
1066
+ lines.push("");
1067
+ const identity = foldedIdentity(job);
1068
+ if (identity.length > 0) {
1069
+ lines.push(`Runtime identity folded onto the job: ${identity.join(", ")}.`);
1070
+ lines.push("");
1071
+ }
1072
+ lines.push("**Mapped**");
1073
+ lines.push("");
1074
+ lines.push(...mdTable(byKind(job.items, ["mapped", "defaulted"]), ["legacy field", "new field", "value / note"]));
1075
+ lines.push("**Dropped / not imported**");
1076
+ lines.push("");
1077
+ lines.push(...mdTable(byKind(job.items, ["dropped", "state", "unrecognized"]), ["legacy field", "new field", "reason"]));
1078
+ if (job.warnings.length > 0) {
1079
+ lines.push("**Check before enabling**");
1080
+ lines.push("");
1081
+ for (const w of job.warnings)
1082
+ lines.push(`- ${w}`);
1083
+ lines.push("");
1084
+ }
1085
+ }
1086
+ if (plan.rejected.length > 0) {
1087
+ lines.push("## Rejected jobs (nothing was written for these)");
1088
+ lines.push("");
1089
+ for (const r of plan.rejected)
1090
+ lines.push(`- \`${r.file}\` \u2014 ${r.reason}`);
1091
+ lines.push("");
1092
+ }
1093
+ lines.push("## Settings");
1094
+ lines.push("");
1095
+ lines.push("**Mapped**");
1096
+ lines.push("");
1097
+ lines.push(...mdTable(byKind(plan.settings.items, ["mapped", "defaulted"]), ["legacy key", "new key", "value / note"]));
1098
+ lines.push("**Dropped**");
1099
+ lines.push("");
1100
+ lines.push(...mdTable(byKind(plan.settings.items, ["dropped", "state", "unrecognized"]), ["legacy key", "new key", "reason"]));
1101
+ if (plan.pauseEntries.length > 0) {
1102
+ lines.push("**Scheduler pause state carried over**");
1103
+ lines.push("");
1104
+ for (const e of plan.pauseEntries)
1105
+ lines.push(`- \`${e.source}\` \u2014 ${e.reason} (since ${e.since})`);
1106
+ lines.push("");
1107
+ }
1108
+ lines.push("## Verification checklist");
1109
+ lines.push("");
1110
+ const checklist = [
1111
+ "`teamos jobs list` shows every job in the table above, with the same enabled state this report records.",
1112
+ "For each job, `teamos jobs show <id>` prints the same `schedule` string as its legacy file \u2014 compare them character by character.",
1113
+ "Next-run times (`teamos jobs show <id>`) land at the wall-clock time the legacy `scheduleHuman` label described.",
1114
+ "Every path in `channels.directory` exists on this host.",
1115
+ "No imported job names a `model`, so every one runs on `engine.defaultModel`. Set `model` on a job that needs a specific one.",
1116
+ "Any job that carried env vars has the right values \u2014 this report never prints them.",
1117
+ 'DST: a `0 2 * * *`-shaped job used to be SKIPPED on the spring-forward day and now fires at 03:00 instead; a `*/N` job used to re-fire across the repeated hour on the fall-back day and now fires once. See "Behavior changes to accept before you resume" in docs/cutover.md.'
1118
+ ];
1119
+ if (plan.jobs.some((j) => j.job.persona !== void 0)) {
1120
+ checklist.push("One or more jobs got a PLACEHOLDER `persona` seeded from a legacy `employee` name (named in the job sections above). It is appended to the system prompt on every turn \u2014 replace the text or delete the field.");
1121
+ }
1122
+ if (plan.jobs.some((j) => j.warnings.some((w) => w.includes("legacy engine profile")))) {
1123
+ checklist.push("One or more jobs named a legacy engine profile whose settings this import cannot read. Re-enter what those runs need \u2014 `model`, `persona`, `pluginDirs`, `mcpServers` \u2014 on the job itself.");
1124
+ }
1125
+ if (plan.jobs.some((j) => j.warnings.some((w) => w.includes("skipIfRunning")))) {
1126
+ checklist.push("One or more jobs allowed overlapping runs (`skipIfRunning: false`). The new scheduler never overlaps two runs of one job \u2014 confirm nothing depended on that.");
1127
+ }
1128
+ if (plan.jobs.some((j) => j.warnings.some((w) => w.includes("already exists in the target home")))) {
1129
+ checklist.push("A legacy job id collided with a job already in the target home (named in that job's section above). Confirm it is the same job and not an unrelated one that happens to share the id.");
1130
+ }
1131
+ if (plan.settings.warnings.some((w) => w.includes("pause window"))) {
1132
+ checklist.push("Legacy quiet-hours pause windows are gone: decide whether any job needs `ignorePauses` or a narrower schedule.");
1133
+ }
1134
+ if (plan.pauseAll) {
1135
+ checklist.push("Jobs are imported disabled. Enable them one at a time with `teamos jobs resume <id>` once the parallel run looks right.");
1136
+ }
1137
+ for (const c of checklist)
1138
+ lines.push(`- [ ] ${c}`);
1139
+ lines.push("");
1140
+ lines.push("Run history, `lastRun`, and failure state are deliberately absent: the new home starts fresh.");
1141
+ lines.push("");
1142
+ return lines.join("\n");
1143
+ }
1144
+
1145
+ // packages/shared/dist/settings-defaults.js
1146
+ var DEFAULT_SETTINGS = {
1147
+ server: {
1148
+ bindHost: "127.0.0.1",
1149
+ allowedHosts: []
1150
+ },
1151
+ retention: {
1152
+ days: 14
1153
+ },
1154
+ scheduler: {
1155
+ maxConcurrentJobs: 3
1156
+ },
1157
+ engine: {
1158
+ defaultModel: "",
1159
+ maxRetries: 2,
1160
+ retryBaseDelayMs: 1e3,
1161
+ // 25 min of silence before the no-stdout watchdog kills the spawn.
1162
+ noProgressTimeoutMs: 15e5
1163
+ },
1164
+ channels: {
1165
+ directory: {},
1166
+ mcp: {}
1167
+ },
1168
+ slack: {
1169
+ enabled: true,
1170
+ // Empty = no persona on Slack turns, which is the pre-persona behavior:
1171
+ // the spawn gets no --append-system-prompt at all.
1172
+ persona: "",
1173
+ progress: "minimal",
1174
+ requireMention: true,
1175
+ reactionTriggers: ["robot_face"],
1176
+ ackEmoji: "eyes",
1177
+ doneEmoji: "white_check_mark",
1178
+ errorEmoji: "x",
1179
+ historyLimit: 20,
1180
+ allowUnmappedChannels: false,
1181
+ dmAllowlist: [],
1182
+ // The deny universe for claude.ai account connectors — see
1183
+ // SlackSettings.knownConnectors. Deny mode can only subtract a name it
1184
+ // knows, so an unlisted connector is inherited by every deny-mode
1185
+ // channel. Listing an absent connector is a harmless no-op, so this
1186
+ // default deliberately over-lists.
1187
+ //
1188
+ // claude_ai_Slack is here for a specific reason: it posts to Slack AS THE
1189
+ // OPERATOR. Shipping it in the deny universe makes it denied by default in
1190
+ // every deny-mode channel, so impersonation requires a deliberate grant.
1191
+ knownConnectors: [
1192
+ "claude_ai_Gmail",
1193
+ "claude_ai_Google_Calendar",
1194
+ "claude_ai_Google_Drive",
1195
+ "claude_ai_Krisp",
1196
+ "claude_ai_Notion",
1197
+ "claude_ai_Slack",
1198
+ "claude_ai_Supabase",
1199
+ "claude_ai_Vercel"
1200
+ ]
1201
+ }
1202
+ };
1203
+
1204
+ // packages/cli/dist/client.js
1205
+ import fs from "node:fs";
1206
+
1207
+ // packages/cli/dist/paths.js
1208
+ import os from "node:os";
1209
+ import path from "node:path";
1210
+ function teamworkHomeDir(env = process.env) {
1211
+ const raw = env.TEAMWORK_HOME;
1212
+ if (raw && path.isAbsolute(raw))
1213
+ return raw;
1214
+ return path.join(os.homedir(), ".teamwork");
1215
+ }
1216
+ var envFilePath = (env) => path.join(teamworkHomeDir(env), ".env");
1217
+ var logsDir = (env) => path.join(teamworkHomeDir(env), "logs");
1218
+ var launchAgentsDir = () => path.join(os.homedir(), "Library", "LaunchAgents");
1219
+
1220
+ // packages/cli/dist/client.js
1221
+ var SESSION_COOKIE_NAME = "__Host-session";
1222
+ function readAuthToken(env = process.env, warn = (m) => console.error(m)) {
1223
+ const fromEnv = env.AUTH_TOKEN?.trim();
1224
+ if (fromEnv)
1225
+ return fromEnv;
1226
+ const file = envFilePath(env);
1227
+ let raw;
1228
+ try {
1229
+ raw = fs.readFileSync(file, "utf8");
1230
+ } catch (err) {
1231
+ const code2 = err.code;
1232
+ if (code2 !== "ENOENT") {
1233
+ warn(`teamos: could not read the teamwork env file (${code2 ?? "unknown"}); continuing unauthenticated`);
1234
+ }
1235
+ return null;
1236
+ }
1237
+ const value = parseEnvFile(raw).find((e) => e.key === "AUTH_TOKEN")?.value.trim();
1238
+ return value && value.length > 0 ? value : null;
1239
+ }
1240
+ function extractSessionCookie(setCookie) {
1241
+ if (!setCookie)
1242
+ return null;
1243
+ const m = new RegExp(`${SESSION_COOKIE_NAME}=([^;]+)`).exec(setCookie);
1244
+ return m ? m[1] : null;
1245
+ }
1246
+ var GatewayClient = class {
1247
+ base;
1248
+ timeoutMs;
1249
+ warn;
1250
+ token;
1251
+ cookie = null;
1252
+ constructor(opts) {
1253
+ this.base = `http://${opts.host ?? "127.0.0.1"}:${opts.port}`;
1254
+ this.timeoutMs = opts.timeoutMs ?? 1e4;
1255
+ this.warn = opts.warn ?? ((m) => console.error(m));
1256
+ this.token = opts.authToken;
1257
+ }
1258
+ resolveToken() {
1259
+ if (this.token === void 0)
1260
+ this.token = readAuthToken(process.env, this.warn);
1261
+ return this.token;
1262
+ }
1263
+ /** Exchange AUTH_TOKEN for a session cookie. Returns null on any failure,
1264
+ * having already explained the failure on stderr. */
1265
+ async login(token) {
1266
+ let res;
1267
+ try {
1268
+ res = await fetch(`${this.base}/api/auth/login`, {
1269
+ method: "POST",
1270
+ headers: { "Content-Type": "application/json" },
1271
+ body: JSON.stringify({ token }),
1272
+ signal: AbortSignal.timeout(this.timeoutMs)
1273
+ });
1274
+ } catch (err) {
1275
+ this.warn(`teamos: login request failed: ${errText(err)}`);
1276
+ return null;
1277
+ }
1278
+ if (res.status === 204)
1279
+ return null;
1280
+ if (!res.ok) {
1281
+ this.warn(res.status === 401 ? "teamos: the gateway rejected AUTH_TOKEN \u2014 the CLI and the gateway disagree on the token" : `teamos: gateway login returned HTTP ${res.status}`);
1282
+ return null;
1283
+ }
1284
+ const cookie = extractSessionCookie(res.headers.get("set-cookie"));
1285
+ if (!cookie) {
1286
+ this.warn("teamos: gateway login succeeded but returned no session cookie");
1287
+ return null;
1288
+ }
1289
+ return cookie;
1290
+ }
1291
+ async send(path11, init, cookie) {
1292
+ const headers = {
1293
+ "Content-Type": "application/json",
1294
+ ...init.headers
1295
+ };
1296
+ if (cookie)
1297
+ headers.Cookie = `${SESSION_COOKIE_NAME}=${cookie}`;
1298
+ try {
1299
+ const res = await fetch(`${this.base}${path11}`, {
1300
+ ...init,
1301
+ headers,
1302
+ signal: AbortSignal.timeout(this.timeoutMs)
1303
+ });
1304
+ const data = await res.json().catch(() => null);
1305
+ return { ok: res.ok, status: res.status, data };
1306
+ } catch (err) {
1307
+ return { ok: false, status: 0, data: null, error: errText(err) };
1308
+ }
1309
+ }
1310
+ /**
1311
+ * Make an API request, logging in first when AUTH_TOKEN is configured.
1312
+ *
1313
+ * A 401 on a request that already carried a cookie triggers exactly one
1314
+ * re-login and retry — that covers a gateway restart under a rotated
1315
+ * AUTH_TOKEN mid-process. A second 401 is returned to the caller rather than
1316
+ * retried, so a genuine credential mismatch fails fast instead of looping.
1317
+ */
1318
+ async request(path11, init = {}) {
1319
+ const token = this.resolveToken();
1320
+ if (!token)
1321
+ return this.send(path11, init, null);
1322
+ if (this.cookie === null)
1323
+ this.cookie = await this.login(token);
1324
+ const first = await this.send(path11, init, this.cookie);
1325
+ if (first.status !== 401 || this.cookie === null)
1326
+ return first;
1327
+ this.cookie = await this.login(token);
1328
+ return this.send(path11, init, this.cookie);
1329
+ }
1330
+ get(path11) {
1331
+ return this.request(path11, { method: "GET" });
1332
+ }
1333
+ post(path11, body) {
1334
+ return this.request(path11, {
1335
+ method: "POST",
1336
+ ...body === void 0 ? {} : { body: JSON.stringify(body) }
1337
+ });
1338
+ }
1339
+ put(path11, body) {
1340
+ return this.request(path11, { method: "PUT", body: JSON.stringify(body) });
1341
+ }
1342
+ delete(path11) {
1343
+ return this.request(path11, { method: "DELETE" });
1344
+ }
1345
+ };
1346
+ function errText(err) {
1347
+ return err instanceof Error ? err.message : String(err);
1348
+ }
1349
+ function describeFailure(res, base) {
1350
+ if (res.status === 0) {
1351
+ return `Gateway unreachable at ${base} (${res.error ?? "no response"}). Is it running? Try: teamos status`;
1352
+ }
1353
+ if (res.status === 401) {
1354
+ return "Unauthorized \u2014 the gateway has AUTH_TOKEN set and the CLI could not authenticate.";
1355
+ }
1356
+ const detail = res.data !== null && typeof res.data === "object" && typeof res.data.error === "string" ? res.data.error : null;
1357
+ return detail ? `HTTP ${res.status}: ${detail}` : `HTTP ${res.status}`;
1358
+ }
1359
+
1360
+ // packages/cli/dist/context.js
1361
+ var EXIT_OK = 0;
1362
+ var EXIT_FAILURE = 1;
1363
+ var EXIT_USAGE = 2;
1364
+ var DEFAULT_PORT = 7463;
1365
+ function resolvePort(args, env = process.env, warn = (m) => console.error(m)) {
1366
+ const fromFlag = args.flags.port;
1367
+ if (fromFlag !== void 0) {
1368
+ if (typeof fromFlag !== "string" || !isPort(fromFlag)) {
1369
+ return { error: `--port must be a TCP port between 1 and 65535 (got ${JSON.stringify(fromFlag)})` };
1370
+ }
1371
+ return Number(fromFlag);
1372
+ }
1373
+ return configuredPort(env, warn);
1374
+ }
1375
+ function configuredPort(env = process.env, warn = (m) => console.error(m)) {
1376
+ const fromEnv = env.PORT;
1377
+ if (fromEnv !== void 0 && fromEnv.trim() !== "") {
1378
+ if (isPort(fromEnv))
1379
+ return Number(fromEnv);
1380
+ warn(`teamos: PORT in the environment is not a valid port (${JSON.stringify(fromEnv)}); ignoring it`);
1381
+ }
1382
+ const fromFile = readPortFromEnvFile(env, warn);
1383
+ if (fromFile !== null)
1384
+ return fromFile;
1385
+ return DEFAULT_PORT;
1386
+ }
1387
+ function isPort(raw) {
1388
+ const trimmed = raw.trim();
1389
+ if (!/^\d+$/.test(trimmed))
1390
+ return false;
1391
+ const n = Number(trimmed);
1392
+ return n >= 1 && n <= 65535;
1393
+ }
1394
+ function readPortFromEnvFile(env, warn) {
1395
+ let raw;
1396
+ try {
1397
+ raw = fs2.readFileSync(envFilePath(env), "utf8");
1398
+ } catch (err) {
1399
+ const code2 = err.code;
1400
+ if (code2 !== "ENOENT") {
1401
+ warn(`teamos: could not read the teamwork env file for PORT (${code2 ?? "unknown"}); using the default`);
1402
+ }
1403
+ return null;
1404
+ }
1405
+ const value = parseEnvFile(raw).find((e) => e.key === "PORT")?.value;
1406
+ if (value === void 0)
1407
+ return null;
1408
+ if (!isPort(value)) {
1409
+ warn(`teamos: PORT in the teamwork env file is not a valid port (${JSON.stringify(value)}); using the default`);
1410
+ return null;
1411
+ }
1412
+ return Number(value);
1413
+ }
1414
+ function makeClient(port, err) {
1415
+ return new GatewayClient({ port, warn: err });
1416
+ }
1417
+
1418
+ // packages/cli/dist/commands/deploy.js
1419
+ import { execFile as execFile3 } from "node:child_process";
1420
+ import fs6 from "node:fs";
1421
+ import path5 from "node:path";
1422
+ import { promisify as promisify3 } from "node:util";
1423
+
1424
+ // packages/cli/dist/repo.js
1425
+ import fs3 from "node:fs";
1426
+ import path2 from "node:path";
1427
+ import { fileURLToPath } from "node:url";
1428
+ var ROOT_PACKAGE_NAME = "teamwork-os";
1429
+ function findRoot(startDir) {
1430
+ let dir = startDir;
1431
+ for (let i = 0; i < 12; i++) {
1432
+ const manifest = path2.join(dir, "package.json");
1433
+ if (fs3.existsSync(manifest)) {
1434
+ try {
1435
+ const parsed = JSON.parse(fs3.readFileSync(manifest, "utf8"));
1436
+ if (parsed.name === ROOT_PACKAGE_NAME)
1437
+ return dir;
1438
+ } catch {
1439
+ }
1440
+ }
1441
+ const parent = path2.dirname(dir);
1442
+ if (parent === dir)
1443
+ break;
1444
+ dir = parent;
1445
+ }
1446
+ return null;
1447
+ }
1448
+ var cached = null;
1449
+ function repoRoot() {
1450
+ if (cached !== null)
1451
+ return cached;
1452
+ const here = path2.dirname(fileURLToPath(import.meta.url));
1453
+ cached = findRoot(here) ?? path2.resolve(here, "..", "..", "..");
1454
+ return cached;
1455
+ }
1456
+
1457
+ // packages/cli/dist/commands/lifecycle.js
1458
+ import { execFile as execFile2, spawn } from "node:child_process";
1459
+ import fs5 from "node:fs";
1460
+ import path4 from "node:path";
1461
+ import { promisify as promisify2 } from "node:util";
1462
+
1463
+ // packages/cli/dist/launchd.js
1464
+ import { execFile } from "node:child_process";
1465
+ import fs4 from "node:fs";
1466
+ import os2 from "node:os";
1467
+ import path3 from "node:path";
1468
+ import { promisify } from "node:util";
1469
+ var execFileAsync = promisify(execFile);
1470
+ var GATEWAY_LABEL = "com.teamwork-os.gateway";
1471
+ var WATCHDOG_LABEL = "com.teamwork-os.watchdog";
1472
+ var KNOWN_GATEWAY_LABELS = Object.freeze([
1473
+ GATEWAY_LABEL,
1474
+ "com.claude-teamwork.gateway",
1475
+ "com.claude-cron.dashboard"
1476
+ ]);
1477
+ function xmlEscape(value) {
1478
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
1479
+ }
1480
+ function shq(value) {
1481
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1482
+ }
1483
+ function gatewayWrapperPath(home) {
1484
+ return path3.join(home, "launch-gateway.sh");
1485
+ }
1486
+ function watchdogScriptPath(home) {
1487
+ return path3.join(home, "teamos-watchdog.sh");
1488
+ }
1489
+ function stopSentinelPath(home) {
1490
+ return path3.join(home, "results", "gateway-stopped");
1491
+ }
1492
+ function renderGatewayWrapper(opts) {
1493
+ const entry = path3.join(opts.repoRoot, "packages", "gateway", "dist", "index.js");
1494
+ return `#!/bin/bash
1495
+ # Generated by \`teamos install\`. Edits are overwritten on the next install.
1496
+ set -euo pipefail
1497
+
1498
+ export HOME=${shq(os2.homedir())}
1499
+ export TEAMWORK_HOME=${shq(opts.teamworkHome)}
1500
+ export NODE_ENV="\${NODE_ENV:-production}"
1501
+ export PORT="\${PORT:-${opts.port}}"
1502
+ export PATH="$(dirname ${shq(opts.nodeBin)}):/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
1503
+
1504
+ cd ${shq(opts.repoRoot)}
1505
+
1506
+ if [ ! -f ${shq(entry)} ]; then
1507
+ echo "gateway not built: ${entry} is missing. Run: npm run build" >&2
1508
+ exit 78
1509
+ fi
1510
+
1511
+ exec ${shq(opts.nodeBin)} ${shq(entry)} --static
1512
+ `;
1513
+ }
1514
+ function renderGatewayPlist(opts) {
1515
+ const logs = path3.join(opts.teamworkHome, "logs");
1516
+ return renderPlist({
1517
+ label: GATEWAY_LABEL,
1518
+ programArguments: ["/bin/bash", gatewayWrapperPath(opts.teamworkHome)],
1519
+ environment: {
1520
+ PORT: String(opts.port),
1521
+ TEAMWORK_HOME: opts.teamworkHome,
1522
+ NODE_ENV: "production"
1523
+ },
1524
+ stdoutPath: path3.join(logs, "gateway-launchd.log"),
1525
+ stderrPath: path3.join(logs, "gateway-launchd-error.log"),
1526
+ runAtLoad: true,
1527
+ // KeepAlive only on a *failed* exit: a deliberate `teamos stop` exits 0
1528
+ // and must stay stopped, whereas a crash must come back.
1529
+ keepAliveOnCrashOnly: true,
1530
+ throttleInterval: 5
1531
+ });
1532
+ }
1533
+ var WATCHDOG_DEFAULT_INTERVAL_SEC = 60;
1534
+ var WATCHDOG_DEFAULT_FAILURE_THRESHOLD = 3;
1535
+ function renderWatchdogScript(opts) {
1536
+ const stateFile = path3.join(opts.teamworkHome, "results", "watchdog-failures");
1537
+ const label = opts.gatewayLabel ?? GATEWAY_LABEL;
1538
+ const logFile = path3.join(opts.teamworkHome, "logs", "watchdog.log");
1539
+ const sentinel = stopSentinelPath(opts.teamworkHome);
1540
+ return `#!/bin/bash
1541
+ # Generated by \`teamos install\`. Edits are overwritten on the next install.
1542
+ #
1543
+ # Probe the gateway's auth-exempt health endpoint. Kickstart the gateway only
1544
+ # after ${opts.failureThreshold} consecutive failures, so a transient stall or an
1545
+ # in-progress deploy restart is never mistaken for a dead gateway.
1546
+ set -uo pipefail
1547
+
1548
+ STATE=${shq(stateFile)}
1549
+ LOG=${shq(logFile)}
1550
+ STOPPED=${shq(sentinel)}
1551
+ URL="http://127.0.0.1:${opts.port}/api/health"
1552
+ THRESHOLD=${opts.failureThreshold}
1553
+ # Quoted like every other interpolated string here. The label reaches this
1554
+ # through \`gatewayLabel\`, which comes from findManagedLabel scanning loaded
1555
+ # services \u2014 a name this file does not control, so it is escaped rather than
1556
+ # trusted to contain no shell metacharacters.
1557
+ LABEL=${shq(label)}
1558
+
1559
+ mkdir -p "$(dirname "$STATE")" "$(dirname "$LOG")"
1560
+
1561
+ log() { echo "[$(date -u +%Y-%m-%dT%H:%M:%SZ)] $*" >> "$LOG"; }
1562
+
1563
+ # A deliberate \`teamos stop\` leaves this marker. Probing past it would read an
1564
+ # intentionally-down gateway as a wedged one and kickstart it back, undoing the
1565
+ # stop every ${opts.intervalSec * opts.failureThreshold}s. Clear the failure streak too, so the
1566
+ # gateway is judged fresh once \`teamos start\` removes the marker.
1567
+ if [ -e "$STOPPED" ]; then
1568
+ echo 0 > "$STATE"
1569
+ exit 0
1570
+ fi
1571
+
1572
+ if curl --silent --show-error --fail --max-time 10 "$URL" > /dev/null 2>&1; then
1573
+ # Any success clears the streak. Without this a gateway that flaps once an
1574
+ # hour would eventually accumulate its way to a spurious restart.
1575
+ echo 0 > "$STATE"
1576
+ exit 0
1577
+ fi
1578
+
1579
+ FAILURES=$(cat "$STATE" 2>/dev/null || echo 0)
1580
+ case "$FAILURES" in (*[!0-9]*|'') FAILURES=0 ;; esac
1581
+ FAILURES=$((FAILURES + 1))
1582
+ echo "$FAILURES" > "$STATE"
1583
+ log "health probe failed ($FAILURES/$THRESHOLD)"
1584
+
1585
+ if [ "$FAILURES" -lt "$THRESHOLD" ]; then
1586
+ exit 0
1587
+ fi
1588
+
1589
+ # Reset before kickstarting: the next probe should judge the NEW process on
1590
+ # its own merits, and a stuck counter would kick every single interval.
1591
+ echo 0 > "$STATE"
1592
+ UID_NUM=$(id -u)
1593
+ log "kickstarting $LABEL after $THRESHOLD consecutive failures"
1594
+ ${opts.launchctlBin ?? "/bin/launchctl"} kickstart -k "gui/\${UID_NUM}/\${LABEL}" >> "$LOG" 2>&1 || log "kickstart failed"
1595
+ `;
1596
+ }
1597
+ function renderWatchdogPlist(opts) {
1598
+ const logs = path3.join(opts.teamworkHome, "logs");
1599
+ return renderPlist({
1600
+ label: WATCHDOG_LABEL,
1601
+ programArguments: ["/bin/bash", watchdogScriptPath(opts.teamworkHome)],
1602
+ environment: { TEAMWORK_HOME: opts.teamworkHome },
1603
+ stdoutPath: path3.join(logs, "watchdog-launchd.log"),
1604
+ stderrPath: path3.join(logs, "watchdog-launchd-error.log"),
1605
+ runAtLoad: true,
1606
+ // The watchdog is a one-shot probe, not a daemon: it must NOT be kept
1607
+ // alive, or launchd would respawn it in a tight loop the instant it exits.
1608
+ keepAliveOnCrashOnly: false,
1609
+ startInterval: opts.intervalSec
1610
+ });
1611
+ }
1612
+ function renderPlist(spec) {
1613
+ const args = spec.programArguments.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
1614
+ const env = Object.entries(spec.environment).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
1615
+ <string>${xmlEscape(v)}</string>`).join("\n");
1616
+ const keepAlive = spec.keepAliveOnCrashOnly ? " <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n" : "";
1617
+ const throttle = spec.throttleInterval === void 0 ? "" : ` <key>ThrottleInterval</key>
1618
+ <integer>${spec.throttleInterval}</integer>
1619
+ `;
1620
+ const startInterval = spec.startInterval === void 0 ? "" : ` <key>StartInterval</key>
1621
+ <integer>${spec.startInterval}</integer>
1622
+ `;
1623
+ return `<?xml version="1.0" encoding="UTF-8"?>
1624
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1625
+ <plist version="1.0">
1626
+ <dict>
1627
+ <key>Label</key>
1628
+ <string>${xmlEscape(spec.label)}</string>
1629
+ <key>ProgramArguments</key>
1630
+ <array>
1631
+ ${args}
1632
+ </array>
1633
+ <key>EnvironmentVariables</key>
1634
+ <dict>
1635
+ ${env}
1636
+ </dict>
1637
+ ${keepAlive} <key>RunAtLoad</key>
1638
+ <${spec.runAtLoad ? "true" : "false"}/>
1639
+ <key>StandardOutPath</key>
1640
+ <string>${xmlEscape(spec.stdoutPath)}</string>
1641
+ <key>StandardErrorPath</key>
1642
+ <string>${xmlEscape(spec.stderrPath)}</string>
1643
+ ${throttle}${startInterval} <key>ProcessType</key>
1644
+ <string>Standard</string>
1645
+ </dict>
1646
+ </plist>
1647
+ `;
1648
+ }
1649
+ function plistPath(agentsDir, label) {
1650
+ return path3.join(agentsDir, `${label}.plist`);
1651
+ }
1652
+ function buildKickstartArgs(uid, label) {
1653
+ return ["kickstart", "-k", `gui/${uid}/${label}`];
1654
+ }
1655
+ function buildKillArgs(uid, label, signal = "SIGTERM") {
1656
+ return ["kill", signal, `gui/${uid}/${label}`];
1657
+ }
1658
+ function buildBootstrapArgs(uid, plist) {
1659
+ return ["bootstrap", `gui/${uid}`, plist];
1660
+ }
1661
+ function buildBootoutArgs(uid, label) {
1662
+ return ["bootout", `gui/${uid}/${label}`];
1663
+ }
1664
+ function currentUid() {
1665
+ return process.getuid?.() ?? null;
1666
+ }
1667
+ function isDarwin() {
1668
+ return process.platform === "darwin";
1669
+ }
1670
+ async function findManagedLabel(agentsDir = launchAgentsDir(), warn = (m) => console.error(m)) {
1671
+ if (!isDarwin())
1672
+ return null;
1673
+ for (const label of KNOWN_GATEWAY_LABELS) {
1674
+ if (!fs4.existsSync(plistPath(agentsDir, label)))
1675
+ continue;
1676
+ try {
1677
+ const { stdout } = await execFileAsync("/bin/launchctl", ["list", label]);
1678
+ if (stdout.includes(label))
1679
+ return label;
1680
+ } catch (err) {
1681
+ if (err.code === "ENOENT") {
1682
+ warn("teamos: launchctl not found; cannot detect a launchd-managed gateway");
1683
+ return null;
1684
+ }
1685
+ }
1686
+ }
1687
+ return null;
1688
+ }
1689
+ async function bootstrap(plist) {
1690
+ const uid = currentUid();
1691
+ if (uid === null)
1692
+ throw new Error("launchctl bootstrap requires a POSIX uid");
1693
+ await execFileAsync("/bin/launchctl", buildBootstrapArgs(uid, plist));
1694
+ }
1695
+ async function bootout(label) {
1696
+ const uid = currentUid();
1697
+ if (uid === null)
1698
+ throw new Error("launchctl bootout requires a POSIX uid");
1699
+ await execFileAsync("/bin/launchctl", buildBootoutArgs(uid, label));
1700
+ }
1701
+
1702
+ // packages/cli/dist/commands/lifecycle.js
1703
+ var execFileAsync2 = promisify2(execFile2);
1704
+ function planLifecycle(facts) {
1705
+ const { intent, managedLabel, listeningPid } = facts;
1706
+ if (intent === "start") {
1707
+ if (listeningPid !== null) {
1708
+ return { kind: "noop", reason: `already running (pid ${listeningPid})` };
1709
+ }
1710
+ return managedLabel ? { kind: "kickstart", label: managedLabel } : { kind: "spawn" };
1711
+ }
1712
+ if (intent === "stop") {
1713
+ if (managedLabel)
1714
+ return { kind: "stop-managed", label: managedLabel };
1715
+ if (listeningPid !== null)
1716
+ return { kind: "signal", pid: listeningPid };
1717
+ return { kind: "noop", reason: "not running" };
1718
+ }
1719
+ if (managedLabel)
1720
+ return { kind: "kickstart", label: managedLabel };
1721
+ return { kind: "spawn" };
1722
+ }
1723
+ async function findListeningPid(port, warn = (m) => console.error(m)) {
1724
+ try {
1725
+ const { stdout } = await execFileAsync2("lsof", ["-ti", `tcp:${port}`, "-sTCP:LISTEN"]);
1726
+ const first = stdout.trim().split("\n")[0];
1727
+ if (!first)
1728
+ return null;
1729
+ const pid = Number.parseInt(first, 10);
1730
+ return Number.isNaN(pid) ? null : pid;
1731
+ } catch (err) {
1732
+ if (err.code === "ENOENT") {
1733
+ warn(`teamos: lsof not found; cannot detect a listener on :${port}`);
1734
+ }
1735
+ return null;
1736
+ }
1737
+ }
1738
+ function markStopped(err) {
1739
+ const sentinel = stopSentinelPath(teamworkHomeDir());
1740
+ try {
1741
+ fs5.mkdirSync(path4.dirname(sentinel), { recursive: true });
1742
+ fs5.writeFileSync(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
1743
+ `);
1744
+ } catch (e) {
1745
+ err(`teamos: could not write the stop marker ${sentinel}: ${e.message}`);
1746
+ err(" The watchdog may kickstart the gateway back. Stop it with: teamos uninstall");
1747
+ }
1748
+ }
1749
+ function clearStopped() {
1750
+ try {
1751
+ fs5.rmSync(stopSentinelPath(teamworkHomeDir()), { force: true });
1752
+ } catch {
1753
+ }
1754
+ }
1755
+ async function waitForHealthy(ctx, timeoutMs) {
1756
+ const deadline = Date.now() + timeoutMs;
1757
+ while (Date.now() < deadline) {
1758
+ const res = await ctx.client.get("/api/health");
1759
+ if (res.ok)
1760
+ return true;
1761
+ await new Promise((r) => setTimeout(r, 500));
1762
+ }
1763
+ return false;
1764
+ }
1765
+ async function waitForExit(pid, timeoutMs) {
1766
+ const deadline = Date.now() + timeoutMs;
1767
+ while (Date.now() < deadline) {
1768
+ try {
1769
+ process.kill(pid, 0);
1770
+ } catch {
1771
+ return true;
1772
+ }
1773
+ await new Promise((r) => setTimeout(r, 250));
1774
+ }
1775
+ return false;
1776
+ }
1777
+ function gatewayEntry() {
1778
+ return path4.join(repoRoot(), "packages", "gateway", "dist", "index.js");
1779
+ }
1780
+ async function startCommand(ctx) {
1781
+ return runLifecycle(ctx, "start");
1782
+ }
1783
+ async function stopCommand(ctx) {
1784
+ return runLifecycle(ctx, "stop");
1785
+ }
1786
+ async function restartCommand(ctx) {
1787
+ return runLifecycle(ctx, "restart");
1788
+ }
1789
+ async function runLifecycle(ctx, intent) {
1790
+ const [managedLabel, listeningPid] = await Promise.all([
1791
+ findManagedLabel(void 0, ctx.err),
1792
+ findListeningPid(ctx.port, ctx.err)
1793
+ ]);
1794
+ const action = planLifecycle({ intent, managedLabel, listeningPid });
1795
+ if (intent !== "stop")
1796
+ clearStopped();
1797
+ switch (action.kind) {
1798
+ case "noop":
1799
+ ctx.out(`Gateway: ${action.reason}.`);
1800
+ return EXIT_OK;
1801
+ case "kickstart": {
1802
+ const uid = currentUid();
1803
+ if (uid === null) {
1804
+ ctx.err("teamos: launchctl requires a POSIX uid; cannot manage the service on this platform");
1805
+ return EXIT_FAILURE;
1806
+ }
1807
+ const args = buildKickstartArgs(uid, action.label);
1808
+ ctx.out(`Restarting via launchd: launchctl ${args.join(" ")}`);
1809
+ try {
1810
+ await execFileAsync2("/bin/launchctl", args);
1811
+ } catch (err) {
1812
+ ctx.err(`teamos: launchctl kickstart failed: ${err.message}`);
1813
+ return EXIT_FAILURE;
1814
+ }
1815
+ const healthy = await waitForHealthy(ctx, 3e4);
1816
+ ctx.out(healthy ? "Gateway: running." : "Gateway did not answer within 30s \u2014 check the launchd error log.");
1817
+ return healthy ? EXIT_OK : EXIT_FAILURE;
1818
+ }
1819
+ case "stop-managed": {
1820
+ const uid = currentUid();
1821
+ if (uid === null) {
1822
+ ctx.err("teamos: launchctl requires a POSIX uid; cannot manage the service on this platform");
1823
+ return EXIT_FAILURE;
1824
+ }
1825
+ markStopped(ctx.err);
1826
+ const args = buildKillArgs(uid, action.label);
1827
+ ctx.out(`Stopping via launchd: launchctl ${args.join(" ")}`);
1828
+ try {
1829
+ await execFileAsync2("/bin/launchctl", args);
1830
+ } catch (err) {
1831
+ ctx.err(`teamos: launchctl kill failed: ${err.message}`);
1832
+ clearStopped();
1833
+ return EXIT_FAILURE;
1834
+ }
1835
+ if (listeningPid !== null && !await waitForExit(listeningPid, 1e4)) {
1836
+ ctx.err(`teamos: pid ${listeningPid} did not exit within 10s; the service may still be running.`);
1837
+ return EXIT_FAILURE;
1838
+ }
1839
+ ctx.out("Gateway: stopped. It stays down (the watchdog is paused) until: teamos start");
1840
+ return EXIT_OK;
1841
+ }
1842
+ case "signal": {
1843
+ markStopped(ctx.err);
1844
+ ctx.out(`Stopping gateway (pid ${action.pid})\u2026`);
1845
+ try {
1846
+ process.kill(action.pid, "SIGTERM");
1847
+ } catch (err) {
1848
+ ctx.err(`teamos: could not signal pid ${action.pid}: ${err.message}`);
1849
+ return EXIT_FAILURE;
1850
+ }
1851
+ const exited = await waitForExit(action.pid, 1e4);
1852
+ if (!exited) {
1853
+ ctx.err(`teamos: pid ${action.pid} did not exit within 10s; escalating to SIGKILL`);
1854
+ try {
1855
+ process.kill(action.pid, "SIGKILL");
1856
+ } catch {
1857
+ }
1858
+ }
1859
+ ctx.out("Gateway: stopped.");
1860
+ return EXIT_OK;
1861
+ }
1862
+ case "spawn": {
1863
+ const entry = gatewayEntry();
1864
+ if (!fs5.existsSync(entry)) {
1865
+ ctx.err(`teamos: gateway is not built (${entry} is missing). Run: npm run build`);
1866
+ return EXIT_FAILURE;
1867
+ }
1868
+ if (intent === "restart" && listeningPid !== null) {
1869
+ ctx.out(`Stopping gateway (pid ${listeningPid})\u2026`);
1870
+ try {
1871
+ process.kill(listeningPid, "SIGTERM");
1872
+ } catch {
1873
+ }
1874
+ if (!await waitForExit(listeningPid, 1e4)) {
1875
+ ctx.err(`teamos: pid ${listeningPid} did not exit; refusing to spawn a second gateway on :${ctx.port}`);
1876
+ return EXIT_FAILURE;
1877
+ }
1878
+ }
1879
+ const child = spawn(process.execPath, [entry, "--static"], {
1880
+ cwd: repoRoot(),
1881
+ env: { ...process.env, PORT: String(ctx.port) },
1882
+ detached: true,
1883
+ stdio: "ignore"
1884
+ });
1885
+ child.on("error", (err) => ctx.err(`teamos: gateway spawn failed: ${err.message}`));
1886
+ child.unref();
1887
+ ctx.out("Starting gateway\u2026");
1888
+ const healthy = await waitForHealthy(ctx, 3e4);
1889
+ if (!healthy) {
1890
+ ctx.err("teamos: gateway did not become healthy within 30s.");
1891
+ return EXIT_FAILURE;
1892
+ }
1893
+ ctx.out(`Gateway: running on :${ctx.port}.`);
1894
+ return EXIT_OK;
1895
+ }
1896
+ default: {
1897
+ const unreachable = action;
1898
+ ctx.err(`teamos: unhandled lifecycle action ${JSON.stringify(unreachable)}`);
1899
+ return EXIT_FAILURE;
1900
+ }
1901
+ }
1902
+ }
1903
+
1904
+ // packages/cli/dist/commands/deploy.js
1905
+ var execFileAsync3 = promisify3(execFile3);
1906
+ function isFullyCurrent(state) {
1907
+ return state.incomingCount === 0 && state.headCommit !== null && state.runningCommit !== null && state.runningCommit === state.headCommit;
1908
+ }
1909
+ function describeDeployReason(state) {
1910
+ if (state.incomingCount > 0) {
1911
+ return `${state.incomingCount} incoming commit(s) to deploy.`;
1912
+ }
1913
+ if (state.runningCommit === null) {
1914
+ return "Level with the remote; the running gateway's revision is unknown \u2014 rebuilding and restarting to be sure.";
1915
+ }
1916
+ if (state.headCommit !== null && state.runningCommit !== state.headCommit) {
1917
+ return `Level with the remote, but the gateway is running ${state.runningCommit.slice(0, 7)} and HEAD is ${state.headCommit.slice(0, 7)} \u2014 redeploying the checked-out code.`;
1918
+ }
1919
+ return "Already fully current \u2014 rebuilding and restarting anyway (pass --skip-if-current to skip).";
1920
+ }
1921
+ function isGitCheckout(root) {
1922
+ return fs6.existsSync(path5.join(root, ".git"));
1923
+ }
1924
+ function describeNotACheckout(root) {
1925
+ return [
1926
+ `teamos: ${root} is not a git checkout \u2014 deploy fetches, pulls, and rebuilds a cloned working tree.`,
1927
+ "For an npm-installed teamwork-os, update with: npm install -g teamwork-os@latest && teamos restart"
1928
+ ].join("\n");
1929
+ }
1930
+ async function git(args) {
1931
+ try {
1932
+ const { stdout, stderr } = await execFileAsync3("git", args, { cwd: repoRoot() });
1933
+ return { ok: true, stdout, stderr };
1934
+ } catch (err) {
1935
+ const e = err;
1936
+ return { ok: false, stdout: e.stdout ?? "", stderr: e.stderr ?? e.message ?? "git failed" };
1937
+ }
1938
+ }
1939
+ async function run(cmd, args, ctx) {
1940
+ try {
1941
+ const { stdout, stderr } = await execFileAsync3(cmd, args, { cwd: repoRoot(), maxBuffer: 32 * 1024 * 1024 });
1942
+ if (stdout.trim())
1943
+ ctx.out(stdout.trimEnd());
1944
+ if (stderr.trim())
1945
+ ctx.err(stderr.trimEnd());
1946
+ return true;
1947
+ } catch (err) {
1948
+ const e = err;
1949
+ if (e.stdout?.trim())
1950
+ ctx.out(e.stdout.trimEnd());
1951
+ ctx.err(e.stderr?.trim() || e.message || `${cmd} failed`);
1952
+ return false;
1953
+ }
1954
+ }
1955
+ async function deployCommand(ctx) {
1956
+ const branch = stringFlag(ctx.args, "branch") ?? "main";
1957
+ const dryRun = boolFlag(ctx.args, "dry-run");
1958
+ const skipIfCurrent = boolFlag(ctx.args, "skip-if-current");
1959
+ const root = repoRoot();
1960
+ if (!isGitCheckout(root)) {
1961
+ ctx.err(describeNotACheckout(root));
1962
+ return EXIT_FAILURE;
1963
+ }
1964
+ const dirty = await git(["status", "--porcelain"]);
1965
+ if (dirty.ok && dirty.stdout.trim().length > 0) {
1966
+ ctx.err("teamos: the working tree has uncommitted changes \u2014 refusing to deploy.");
1967
+ ctx.err(dirty.stdout.trimEnd());
1968
+ return EXIT_FAILURE;
1969
+ }
1970
+ const fetched = await git(["fetch", "origin", branch]);
1971
+ if (!fetched.ok) {
1972
+ ctx.err(`teamos: git fetch origin ${branch} failed \u2014 cannot determine deploy state.`);
1973
+ ctx.err(fetched.stderr.trimEnd());
1974
+ return EXIT_FAILURE;
1975
+ }
1976
+ const log = await git(["log", `HEAD..origin/${branch}`, "--oneline"]);
1977
+ const incoming = log.ok ? log.stdout.trim().split("\n").filter((l) => l.length > 0) : [];
1978
+ const headRes = await git(["rev-parse", "HEAD"]);
1979
+ const headCommit = headRes.ok && /^[0-9a-f]{40}$/.test(headRes.stdout.trim()) ? headRes.stdout.trim() : null;
1980
+ const health = await ctx.client.get("/api/health");
1981
+ const rawCommit = health.ok ? health.data?.commit : null;
1982
+ const runningCommit = typeof rawCommit === "string" && /^[0-9a-f]{40}$/.test(rawCommit) ? rawCommit : null;
1983
+ const state = { incomingCount: incoming.length, runningCommit, headCommit };
1984
+ if (skipIfCurrent && isFullyCurrent(state)) {
1985
+ ctx.out(`Already up to date and the gateway is on the checked-out commit (${headCommit.slice(0, 7)}) \u2014 nothing to deploy.`);
1986
+ return EXIT_OK;
1987
+ }
1988
+ ctx.out(describeDeployReason(state));
1989
+ for (const line of incoming)
1990
+ ctx.out(` ${line}`);
1991
+ if (dryRun) {
1992
+ ctx.out("\n--dry-run: stopping before pull, build, and restart.");
1993
+ return EXIT_OK;
1994
+ }
1995
+ if (incoming.length > 0) {
1996
+ ctx.out("\n\u2500\u2500 git pull --ff-only \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
1997
+ if (!await run("git", ["pull", "--ff-only", "origin", branch], ctx)) {
1998
+ ctx.err("teamos: fast-forward pull failed \u2014 the deploy host has diverged from the remote. Aborting.");
1999
+ return EXIT_FAILURE;
2000
+ }
2001
+ }
2002
+ ctx.out("\n\u2500\u2500 npm install \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2003
+ if (!await run("npm", ["install", "--no-audit", "--no-fund"], ctx)) {
2004
+ ctx.err("teamos: dependency install failed \u2014 aborting before the build.");
2005
+ return EXIT_FAILURE;
2006
+ }
2007
+ ctx.out("\n\u2500\u2500 npm run build \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2008
+ if (!await run("npm", ["run", "build"], ctx)) {
2009
+ ctx.err("teamos: build failed \u2014 the running gateway was left untouched.");
2010
+ return EXIT_FAILURE;
2011
+ }
2012
+ ctx.out("\n\u2500\u2500 restart \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
2013
+ const code2 = await restartCommand(ctx);
2014
+ if (code2 !== EXIT_OK)
2015
+ return code2;
2016
+ ctx.out("\nDeploy complete.");
2017
+ return EXIT_OK;
2018
+ }
2019
+
2020
+ // packages/cli/dist/commands/env.js
2021
+ import fs7 from "node:fs";
2022
+ import path6 from "node:path";
2023
+
2024
+ // packages/cli/dist/format.js
2025
+ function cell(value) {
2026
+ if (value === null || value === void 0)
2027
+ return "\u2014";
2028
+ if (typeof value === "boolean")
2029
+ return value ? "yes" : "no";
2030
+ return String(value);
2031
+ }
2032
+ function table(headers, rows) {
2033
+ const body = rows.map((r) => r.map(cell));
2034
+ const widths = headers.map((h, i) => Math.max(h.length, ...body.map((r) => (r[i] ?? "").length), 0));
2035
+ const line = (cells) => cells.map((c, i) => i === cells.length - 1 ? c : c.padEnd(widths[i])).join(" ").trimEnd();
2036
+ return [line(headers), line(widths.map((w) => "\u2500".repeat(w))), ...body.map(line)].join("\n");
2037
+ }
2038
+ function keyValues(pairs) {
2039
+ const width = Math.max(...pairs.map(([k]) => k.length), 0);
2040
+ return pairs.map(([k, v]) => `${`${k}:`.padEnd(width + 1)} ${cell(v)}`).join("\n");
2041
+ }
2042
+ function truncate(value, max) {
2043
+ if (max <= 1 || value.length <= max)
2044
+ return value;
2045
+ return `${value.slice(0, max - 1)}\u2026`;
2046
+ }
2047
+ function relativeTime(iso, now = Date.now()) {
2048
+ if (!iso)
2049
+ return "\u2014";
2050
+ const t = Date.parse(iso);
2051
+ if (Number.isNaN(t))
2052
+ return "\u2014";
2053
+ const deltaSec = Math.round((now - t) / 1e3);
2054
+ const future = deltaSec < 0;
2055
+ const abs = Math.abs(deltaSec);
2056
+ const unit = abs < 60 ? `${abs}s` : abs < 3600 ? `${Math.floor(abs / 60)}m` : abs < 86400 ? `${Math.floor(abs / 3600)}h` : `${Math.floor(abs / 86400)}d`;
2057
+ return future ? `in ${unit}` : `${unit} ago`;
2058
+ }
2059
+
2060
+ // packages/cli/dist/commands/env.js
2061
+ var KEY_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
2062
+ function maskValue(value) {
2063
+ if (value.length === 0)
2064
+ return "(empty)";
2065
+ if (value.length <= 8)
2066
+ return `${"*".repeat(value.length)} (len=${value.length})`;
2067
+ return `${value.slice(0, 2)}${"*".repeat(6)}${value.slice(-2)} (len=${value.length})`;
2068
+ }
2069
+ function readEntries(file) {
2070
+ try {
2071
+ return parseEnvFile(fs7.readFileSync(file, "utf8"));
2072
+ } catch (err) {
2073
+ if (err.code === "ENOENT")
2074
+ return [];
2075
+ throw err;
2076
+ }
2077
+ }
2078
+ function writeEntries(file, entries) {
2079
+ fs7.mkdirSync(path6.dirname(file), { recursive: true });
2080
+ const tmp = `${file}.tmp-${process.pid}`;
2081
+ fs7.writeFileSync(tmp, formatEnvFile(entries), { mode: 384 });
2082
+ fs7.renameSync(tmp, file);
2083
+ fs7.chmodSync(file, 384);
2084
+ }
2085
+ function splitAssignment(raw) {
2086
+ const eq = raw.indexOf("=");
2087
+ if (eq <= 0)
2088
+ return null;
2089
+ return { key: raw.slice(0, eq), value: raw.slice(eq + 1) };
2090
+ }
2091
+ function applyEntryChange(entries, key, value) {
2092
+ const next = entries.filter((e) => e.key !== key);
2093
+ const existed = next.length !== entries.length;
2094
+ if (value === null)
2095
+ return { entries: next, changed: existed };
2096
+ const previous = entries.find((e) => e.key === key);
2097
+ if (previous && previous.value === value)
2098
+ return { entries: [...entries], changed: false };
2099
+ if (existed) {
2100
+ const at = entries.findIndex((e) => e.key === key);
2101
+ next.splice(at, 0, { key, value });
2102
+ } else {
2103
+ next.push({ key, value });
2104
+ }
2105
+ return { entries: next, changed: true };
2106
+ }
2107
+ async function envCommand(ctx) {
2108
+ const [sub, ...rest] = ctx.rest;
2109
+ const file = envFilePath();
2110
+ switch (sub) {
2111
+ case void 0:
2112
+ case "list": {
2113
+ const entries = readEntries(file);
2114
+ if (entries.length === 0) {
2115
+ ctx.out("No environment variables set.");
2116
+ return EXIT_OK;
2117
+ }
2118
+ const reveal = boolFlag(ctx.args, "values");
2119
+ if (reveal) {
2120
+ ctx.err("teamos: printing raw values \u2014 avoid this in a shared terminal or a recorded session.");
2121
+ }
2122
+ ctx.out(table(["KEY", "VALUE"], entries.map((e) => [e.key, reveal ? e.value : maskValue(e.value)])));
2123
+ return EXIT_OK;
2124
+ }
2125
+ case "get": {
2126
+ const key = rest[0];
2127
+ if (!key) {
2128
+ ctx.err("Usage: teamos env get <KEY>");
2129
+ return EXIT_USAGE;
2130
+ }
2131
+ const entry = readEntries(file).find((e) => e.key === key);
2132
+ if (!entry) {
2133
+ ctx.err(`teamos: ${key} is not set`);
2134
+ return EXIT_FAILURE;
2135
+ }
2136
+ ctx.out(entry.value);
2137
+ return EXIT_OK;
2138
+ }
2139
+ case "set": {
2140
+ const assignment = rest[0];
2141
+ if (!assignment) {
2142
+ ctx.err("Usage: teamos env set KEY=VALUE");
2143
+ return EXIT_USAGE;
2144
+ }
2145
+ const parsed = splitAssignment(assignment);
2146
+ if (!parsed) {
2147
+ ctx.err(`Usage: teamos env set KEY=VALUE (got ${JSON.stringify(assignment)})`);
2148
+ return EXIT_USAGE;
2149
+ }
2150
+ if (!KEY_RE2.test(parsed.key)) {
2151
+ ctx.err(`teamos: ${JSON.stringify(parsed.key)} is not a valid environment variable name`);
2152
+ return EXIT_USAGE;
2153
+ }
2154
+ const { entries, changed } = applyEntryChange(readEntries(file), parsed.key, parsed.value);
2155
+ writeEntries(file, entries);
2156
+ ctx.out(changed ? `Set ${parsed.key}.` : `${parsed.key} already had that value.`);
2157
+ return EXIT_OK;
2158
+ }
2159
+ case "unset": {
2160
+ const key = rest[0];
2161
+ if (!key) {
2162
+ ctx.err("Usage: teamos env unset <KEY>");
2163
+ return EXIT_USAGE;
2164
+ }
2165
+ const { entries, changed } = applyEntryChange(readEntries(file), key, null);
2166
+ if (!changed) {
2167
+ ctx.err(`teamos: ${key} is not set`);
2168
+ return EXIT_FAILURE;
2169
+ }
2170
+ writeEntries(file, entries);
2171
+ ctx.out(`Unset ${key}.`);
2172
+ return EXIT_OK;
2173
+ }
2174
+ case "audit": {
2175
+ let raw;
2176
+ try {
2177
+ raw = fs7.readFileSync(file, "utf8");
2178
+ } catch (err) {
2179
+ if (err.code === "ENOENT") {
2180
+ ctx.out("No env file \u2014 nothing to audit.");
2181
+ return EXIT_OK;
2182
+ }
2183
+ throw err;
2184
+ }
2185
+ const unsafe = auditEnvFile(raw);
2186
+ if (unsafe.length === 0) {
2187
+ ctx.out("Every line would survive a POSIX `source`.");
2188
+ return EXIT_OK;
2189
+ }
2190
+ ctx.out(`${unsafe.length} line(s) would break \`source\`:`);
2191
+ for (const line of unsafe) {
2192
+ ctx.out(` line ${line.lineNumber}: ${line.reason}`);
2193
+ }
2194
+ ctx.out("Fix with: teamos env normalize");
2195
+ return EXIT_FAILURE;
2196
+ }
2197
+ case "normalize": {
2198
+ const entries = readEntries(file);
2199
+ if (entries.length === 0) {
2200
+ ctx.out("No env file \u2014 nothing to normalize.");
2201
+ return EXIT_OK;
2202
+ }
2203
+ const before = fs7.readFileSync(file, "utf8");
2204
+ const after = formatEnvFile(entries);
2205
+ if (before === after) {
2206
+ ctx.out("Already in canonical form.");
2207
+ return EXIT_OK;
2208
+ }
2209
+ writeEntries(file, entries);
2210
+ ctx.out(`Rewrote ${entries.length} entries in canonical form.`);
2211
+ return EXIT_OK;
2212
+ }
2213
+ default:
2214
+ ctx.err(`Unknown env subcommand: ${sub}
2215
+ Usage: teamos env <list|get|set|unset|audit|normalize>`);
2216
+ return EXIT_USAGE;
2217
+ }
2218
+ }
2219
+
2220
+ // packages/cli/dist/commands/install.js
2221
+ import fs8 from "node:fs";
2222
+ import path7 from "node:path";
2223
+ function renderInstallArtifacts(opts) {
2224
+ const gatewayOpts = {
2225
+ agentsDir: opts.agentsDir,
2226
+ teamworkHome: opts.teamworkHome,
2227
+ repoRoot: opts.repoRoot,
2228
+ port: opts.port,
2229
+ nodeBin: opts.nodeBin
2230
+ };
2231
+ const watchdogOpts = {
2232
+ ...gatewayOpts,
2233
+ intervalSec: opts.intervalSec,
2234
+ failureThreshold: opts.failureThreshold
2235
+ };
2236
+ return [
2237
+ {
2238
+ path: gatewayWrapperPath(opts.teamworkHome),
2239
+ contents: renderGatewayWrapper(gatewayOpts),
2240
+ mode: 493
2241
+ },
2242
+ {
2243
+ path: watchdogScriptPath(opts.teamworkHome),
2244
+ contents: renderWatchdogScript(watchdogOpts),
2245
+ mode: 493
2246
+ },
2247
+ {
2248
+ path: plistPath(opts.agentsDir, GATEWAY_LABEL),
2249
+ contents: renderGatewayPlist(gatewayOpts),
2250
+ mode: 420
2251
+ },
2252
+ {
2253
+ path: plistPath(opts.agentsDir, WATCHDOG_LABEL),
2254
+ contents: renderWatchdogPlist(watchdogOpts),
2255
+ mode: 420
2256
+ }
2257
+ ];
2258
+ }
2259
+ async function installCommand(ctx) {
2260
+ const agentsDir = stringFlag(ctx.args, "agents-dir") ?? launchAgentsDir();
2261
+ const dryRun = boolFlag(ctx.args, "dry-run");
2262
+ if (!isDarwin() && !dryRun) {
2263
+ ctx.err("teamos: launchd services are macOS-only. On Linux, run the gateway under systemd.");
2264
+ return EXIT_FAILURE;
2265
+ }
2266
+ const interval = intFlag(ctx.args, "watchdog-interval", { min: 10, max: 3600 });
2267
+ if (interval !== void 0 && typeof interval === "object") {
2268
+ ctx.err(`teamos: ${interval.error}`);
2269
+ return EXIT_USAGE;
2270
+ }
2271
+ const threshold = intFlag(ctx.args, "watchdog-failures", { min: 1, max: 20 });
2272
+ if (threshold !== void 0 && typeof threshold === "object") {
2273
+ ctx.err(`teamos: ${threshold.error}`);
2274
+ return EXIT_USAGE;
2275
+ }
2276
+ const intervalSec = interval ?? WATCHDOG_DEFAULT_INTERVAL_SEC;
2277
+ const failureThreshold = threshold ?? WATCHDOG_DEFAULT_FAILURE_THRESHOLD;
2278
+ const artifacts = renderInstallArtifacts({
2279
+ agentsDir,
2280
+ teamworkHome: teamworkHomeDir(),
2281
+ repoRoot: repoRoot(),
2282
+ port: ctx.port,
2283
+ // process.execPath, not `node` on PATH: launchd's PATH is not the
2284
+ // operator's, and an nvm-managed node is invisible to it.
2285
+ nodeBin: process.execPath,
2286
+ intervalSec,
2287
+ failureThreshold
2288
+ });
2289
+ if (dryRun) {
2290
+ for (const a of artifacts) {
2291
+ ctx.out(`\u2500\u2500 ${a.path} (mode ${a.mode.toString(8)}) \u2500\u2500`);
2292
+ ctx.out(a.contents);
2293
+ }
2294
+ return EXIT_OK;
2295
+ }
2296
+ for (const a of artifacts) {
2297
+ fs8.mkdirSync(path7.dirname(a.path), { recursive: true });
2298
+ fs8.writeFileSync(a.path, a.contents, { mode: a.mode });
2299
+ fs8.chmodSync(a.path, a.mode);
2300
+ ctx.out(`Wrote ${a.path}`);
2301
+ }
2302
+ fs8.mkdirSync(path7.join(teamworkHomeDir(), "logs"), { recursive: true });
2303
+ fs8.mkdirSync(path7.join(teamworkHomeDir(), "results"), { recursive: true });
2304
+ for (const label of [GATEWAY_LABEL, WATCHDOG_LABEL]) {
2305
+ const plist = plistPath(agentsDir, label);
2306
+ try {
2307
+ await bootout(label);
2308
+ } catch {
2309
+ }
2310
+ try {
2311
+ await bootstrap(plist);
2312
+ ctx.out(`Loaded ${label}`);
2313
+ } catch (err) {
2314
+ ctx.err(`teamos: could not load ${label}: ${err.message}`);
2315
+ return EXIT_FAILURE;
2316
+ }
2317
+ }
2318
+ ctx.out("");
2319
+ ctx.out(`Gateway will start at login and restart on crash (port ${ctx.port}).`);
2320
+ ctx.out(`Watchdog probes /api/health every ${intervalSec}s and kickstarts the gateway after ${failureThreshold} consecutive failures (~${intervalSec * failureThreshold}s of downtime).`);
2321
+ return EXIT_OK;
2322
+ }
2323
+ async function uninstallCommand(ctx) {
2324
+ const agentsDir = stringFlag(ctx.args, "agents-dir") ?? launchAgentsDir();
2325
+ if (!isDarwin()) {
2326
+ ctx.err("teamos: launchd services are macOS-only; nothing to uninstall.");
2327
+ return EXIT_FAILURE;
2328
+ }
2329
+ let removed = 0;
2330
+ for (const label of [WATCHDOG_LABEL, GATEWAY_LABEL]) {
2331
+ try {
2332
+ await bootout(label);
2333
+ ctx.out(`Unloaded ${label}`);
2334
+ } catch {
2335
+ }
2336
+ const plist = plistPath(agentsDir, label);
2337
+ if (fs8.existsSync(plist)) {
2338
+ fs8.rmSync(plist);
2339
+ ctx.out(`Removed ${plist}`);
2340
+ removed++;
2341
+ }
2342
+ }
2343
+ for (const script of [gatewayWrapperPath(teamworkHomeDir()), watchdogScriptPath(teamworkHomeDir())]) {
2344
+ if (fs8.existsSync(script)) {
2345
+ fs8.rmSync(script);
2346
+ ctx.out(`Removed ${script}`);
2347
+ }
2348
+ }
2349
+ ctx.out(removed === 0 ? "Nothing was installed." : "Uninstalled. Logs under ~/.teamwork/logs were left in place.");
2350
+ return EXIT_OK;
2351
+ }
2352
+
2353
+ // packages/cli/dist/commands/jobs.js
2354
+ import fs9 from "node:fs";
2355
+ var RUNS_LIMIT = { min: 1, max: 200 };
2356
+ function fail(ctx, res) {
2357
+ ctx.err(describeFailure(res, ctx.client.base));
2358
+ return EXIT_FAILURE;
2359
+ }
2360
+ function readJsonInput(source) {
2361
+ let raw;
2362
+ if (source === "-") {
2363
+ try {
2364
+ raw = fs9.readFileSync(0, "utf8");
2365
+ } catch (err) {
2366
+ return { ok: false, error: `could not read stdin: ${err.message}` };
2367
+ }
2368
+ } else if (source.trimStart().startsWith("{")) {
2369
+ raw = source;
2370
+ } else {
2371
+ try {
2372
+ raw = fs9.readFileSync(source, "utf8");
2373
+ } catch (err) {
2374
+ return { ok: false, error: `could not read ${source}: ${err.message}` };
2375
+ }
2376
+ }
2377
+ try {
2378
+ const value = JSON.parse(raw);
2379
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
2380
+ return { ok: false, error: "job definition must be a JSON object" };
2381
+ }
2382
+ return { ok: true, value };
2383
+ } catch (err) {
2384
+ return { ok: false, error: `job definition is not valid JSON: ${err.message}` };
2385
+ }
2386
+ }
2387
+ async function jobsCommand(ctx) {
2388
+ const [sub, ...rest] = ctx.rest;
2389
+ switch (sub) {
2390
+ case void 0:
2391
+ case "list": {
2392
+ const res = await ctx.client.get("/api/jobs");
2393
+ if (!res.ok)
2394
+ return fail(ctx, res);
2395
+ const jobs = Array.isArray(res.data) ? res.data : [];
2396
+ if (jobs.length === 0) {
2397
+ ctx.out("No jobs configured.");
2398
+ return EXIT_OK;
2399
+ }
2400
+ ctx.out(table(["ID", "ENABLED", "SCHEDULE", "LAST", "LAST RUN", "NEXT RUN"], jobs.map((j) => [
2401
+ j.id,
2402
+ j.enabled,
2403
+ // No schedule is a real, supported state (a manual job), not
2404
+ // missing data — `—` would read as "the gateway didn't say".
2405
+ j.schedule ? truncate(j.schedule, 20) : "manual",
2406
+ j.lastStatus,
2407
+ relativeTime(j.lastRun),
2408
+ relativeTime(j.nextRun)
2409
+ ])));
2410
+ return EXIT_OK;
2411
+ }
2412
+ case "show": {
2413
+ const id = rest[0];
2414
+ if (!id)
2415
+ return usage(ctx, "teamos jobs show <id>");
2416
+ const res = await ctx.client.get(`/api/jobs/${encodeURIComponent(id)}`);
2417
+ if (!res.ok)
2418
+ return fail(ctx, res);
2419
+ ctx.out(JSON.stringify(res.data, null, 2));
2420
+ return EXIT_OK;
2421
+ }
2422
+ case "create": {
2423
+ const source = rest[0] ?? stringFlag(ctx.args, "file");
2424
+ if (!source)
2425
+ return usage(ctx, `teamos jobs create <file.json|-|'{"id":\u2026}'>`);
2426
+ const input = readJsonInput(source);
2427
+ if (!input.ok) {
2428
+ ctx.err(`teamos: ${input.error}`);
2429
+ return EXIT_USAGE;
2430
+ }
2431
+ const res = await ctx.client.post("/api/jobs", input.value);
2432
+ if (!res.ok)
2433
+ return fail(ctx, res);
2434
+ const id = res.data?.id;
2435
+ ctx.out(`Created job ${id ?? "(unnamed)"}.`);
2436
+ return EXIT_OK;
2437
+ }
2438
+ case "update": {
2439
+ const id = rest[0];
2440
+ const source = rest[1] ?? stringFlag(ctx.args, "file");
2441
+ if (!id || !source)
2442
+ return usage(ctx, "teamos jobs update <id> <file.json|-|'{\u2026}'>");
2443
+ const input = readJsonInput(source);
2444
+ if (!input.ok) {
2445
+ ctx.err(`teamos: ${input.error}`);
2446
+ return EXIT_USAGE;
2447
+ }
2448
+ const res = await ctx.client.put(`/api/jobs/${encodeURIComponent(id)}`, input.value);
2449
+ if (!res.ok)
2450
+ return fail(ctx, res);
2451
+ ctx.out(`Updated job ${id}.`);
2452
+ return EXIT_OK;
2453
+ }
2454
+ case "delete": {
2455
+ const id = rest[0];
2456
+ if (!id)
2457
+ return usage(ctx, "teamos jobs delete <id>");
2458
+ const res = await ctx.client.delete(`/api/jobs/${encodeURIComponent(id)}`);
2459
+ if (!res.ok)
2460
+ return fail(ctx, res);
2461
+ ctx.out(`Deleted job ${id}.`);
2462
+ return EXIT_OK;
2463
+ }
2464
+ case "run": {
2465
+ const id = rest[0];
2466
+ if (!id)
2467
+ return usage(ctx, "teamos jobs run <id>");
2468
+ const res = await ctx.client.post(`/api/jobs/${encodeURIComponent(id)}/run`);
2469
+ if (!res.ok)
2470
+ return fail(ctx, res);
2471
+ ctx.out(`Dispatched job ${id}. Follow it with: teamos jobs show ${id}`);
2472
+ return EXIT_OK;
2473
+ }
2474
+ case "pause": {
2475
+ const id = rest[0];
2476
+ if (!id)
2477
+ return usage(ctx, 'teamos jobs pause <id> [--reason "..."]');
2478
+ const reason = stringFlag(ctx.args, "reason");
2479
+ const res = await ctx.client.post(`/api/jobs/${encodeURIComponent(id)}/pause`, reason === void 0 ? {} : { reason });
2480
+ if (!res.ok)
2481
+ return fail(ctx, res);
2482
+ ctx.out(`Paused job ${id}.`);
2483
+ return EXIT_OK;
2484
+ }
2485
+ case "resume": {
2486
+ const id = rest[0];
2487
+ if (!id)
2488
+ return usage(ctx, "teamos jobs resume <id>");
2489
+ const res = await ctx.client.post(`/api/jobs/${encodeURIComponent(id)}/resume`);
2490
+ if (!res.ok)
2491
+ return fail(ctx, res);
2492
+ ctx.out(`Resumed job ${id}.`);
2493
+ return EXIT_OK;
2494
+ }
2495
+ case "runs": {
2496
+ const id = rest[0];
2497
+ if (!id)
2498
+ return usage(ctx, "teamos jobs runs <id> [--limit N]");
2499
+ const limit = intFlag(ctx.args, "limit", RUNS_LIMIT);
2500
+ if (limit !== void 0 && typeof limit === "object") {
2501
+ ctx.err(`teamos: ${limit.error}`);
2502
+ return EXIT_USAGE;
2503
+ }
2504
+ const qs = limit === void 0 ? "" : `?limit=${limit}`;
2505
+ const res = await ctx.client.get(`/api/jobs/${encodeURIComponent(id)}/runs${qs}`);
2506
+ if (!res.ok)
2507
+ return fail(ctx, res);
2508
+ const runs = res.data?.runs ?? [];
2509
+ if (runs.length === 0) {
2510
+ ctx.out(`No recorded runs for ${id}.`);
2511
+ return EXIT_OK;
2512
+ }
2513
+ ctx.out(table(["WHEN", "STATUS", "DURATION", "RUN ID"], runs.map((r) => [
2514
+ relativeTime(r.timestamp),
2515
+ r.status,
2516
+ r.durationSeconds === void 0 ? "\u2014" : `${Math.round(r.durationSeconds)}s`,
2517
+ r.resultFile ? r.resultFile.replace(/\.result$/, "") : "\u2014"
2518
+ ])));
2519
+ return EXIT_OK;
2520
+ }
2521
+ case "trace": {
2522
+ const id = rest[0];
2523
+ const runId = rest[1];
2524
+ if (!id || !runId)
2525
+ return usage(ctx, "teamos jobs trace <id> <run-id>");
2526
+ const res = await ctx.client.get(`/api/jobs/${encodeURIComponent(id)}/runs/${encodeURIComponent(runId)}/trace`);
2527
+ if (!res.ok) {
2528
+ if (res.status === 404) {
2529
+ ctx.err(`teamos: no engine trace for run ${runId}. Shell jobs produce no trace; list runs with: teamos jobs runs ${id}`);
2530
+ return EXIT_FAILURE;
2531
+ }
2532
+ return fail(ctx, res);
2533
+ }
2534
+ ctx.out(JSON.stringify(res.data, null, 2));
2535
+ return EXIT_OK;
2536
+ }
2537
+ default:
2538
+ return usage(ctx, "teamos jobs <list|show|create|update|delete|run|pause|resume|runs|trace>", `Unknown jobs subcommand: ${sub}`);
2539
+ }
2540
+ }
2541
+ function usage(ctx, line, prefix) {
2542
+ if (prefix)
2543
+ ctx.err(prefix);
2544
+ ctx.err(`Usage: ${line}`);
2545
+ return EXIT_USAGE;
2546
+ }
2547
+
2548
+ // packages/cli/dist/commands/logs.js
2549
+ import fs10 from "node:fs";
2550
+ import path8 from "node:path";
2551
+ var DEFAULT_LINES = 50;
2552
+ function resolveLogPath(dir, name) {
2553
+ if (name.includes("/") || name.includes("\\") || name === "." || name === "..") {
2554
+ return { error: `log name must not contain a path (got ${JSON.stringify(name)})` };
2555
+ }
2556
+ const withExt = name.endsWith(".log") ? name : `${name}.log`;
2557
+ const resolved = path8.resolve(dir, withExt);
2558
+ if (!resolved.startsWith(path8.resolve(dir) + path8.sep)) {
2559
+ return { error: `log name resolves outside the logs directory (got ${JSON.stringify(name)})` };
2560
+ }
2561
+ return resolved;
2562
+ }
2563
+ function lastLines(content, count) {
2564
+ const lines = content.split("\n");
2565
+ if (lines.length > 0 && lines[lines.length - 1] === "")
2566
+ lines.pop();
2567
+ return count >= lines.length ? lines : lines.slice(lines.length - count);
2568
+ }
2569
+ async function logsCommand(ctx) {
2570
+ const dir = logsDir();
2571
+ const name = ctx.rest[0];
2572
+ if (!name) {
2573
+ let entries;
2574
+ try {
2575
+ entries = fs10.readdirSync(dir).filter((f) => f.endsWith(".log")).sort();
2576
+ } catch (err) {
2577
+ if (err.code === "ENOENT") {
2578
+ ctx.out(`No logs directory yet at ${dir}.`);
2579
+ return EXIT_OK;
2580
+ }
2581
+ throw err;
2582
+ }
2583
+ if (entries.length === 0) {
2584
+ ctx.out(`No logs in ${dir}.`);
2585
+ return EXIT_OK;
2586
+ }
2587
+ ctx.out(table(["LOG", "SIZE"], entries.map((f) => {
2588
+ const size = fs10.statSync(path8.join(dir, f)).size;
2589
+ return [f.replace(/\.log$/, ""), formatBytes(size)];
2590
+ })));
2591
+ ctx.out("\nTail one with: teamos logs <name> [--lines N] [--follow]");
2592
+ return EXIT_OK;
2593
+ }
2594
+ const resolved = resolveLogPath(dir, name);
2595
+ if (typeof resolved !== "string") {
2596
+ ctx.err(`teamos: ${resolved.error}`);
2597
+ return EXIT_USAGE;
2598
+ }
2599
+ const linesFlag = intFlag(ctx.args, "lines", { min: 1, max: 1e5 });
2600
+ if (linesFlag !== void 0 && typeof linesFlag === "object") {
2601
+ ctx.err(`teamos: ${linesFlag.error}`);
2602
+ return EXIT_USAGE;
2603
+ }
2604
+ const count = linesFlag ?? DEFAULT_LINES;
2605
+ let content;
2606
+ try {
2607
+ content = fs10.readFileSync(resolved, "utf8");
2608
+ } catch (err) {
2609
+ if (err.code === "ENOENT") {
2610
+ ctx.err(`teamos: no log named ${name} in ${dir}. List them with: teamos logs`);
2611
+ return EXIT_FAILURE;
2612
+ }
2613
+ throw err;
2614
+ }
2615
+ for (const line of lastLines(content, count))
2616
+ ctx.out(line);
2617
+ if (!boolFlag(ctx.args, "follow"))
2618
+ return EXIT_OK;
2619
+ let offset = Buffer.byteLength(content, "utf8");
2620
+ ctx.out(`
2621
+ \u2014 following ${path8.basename(resolved)} (Ctrl-C to stop) \u2014`);
2622
+ for (; ; ) {
2623
+ await new Promise((r) => setTimeout(r, 500));
2624
+ let size;
2625
+ try {
2626
+ size = fs10.statSync(resolved).size;
2627
+ } catch {
2628
+ continue;
2629
+ }
2630
+ if (size < offset) {
2631
+ ctx.out("\u2014 log truncated, following the new file \u2014");
2632
+ offset = 0;
2633
+ }
2634
+ if (size === offset)
2635
+ continue;
2636
+ const fd = fs10.openSync(resolved, "r");
2637
+ try {
2638
+ const buf = Buffer.alloc(size - offset);
2639
+ fs10.readSync(fd, buf, 0, buf.length, offset);
2640
+ offset = size;
2641
+ const text = buf.toString("utf8");
2642
+ for (const line of text.split("\n")) {
2643
+ if (line.length > 0)
2644
+ ctx.out(line);
2645
+ }
2646
+ } finally {
2647
+ fs10.closeSync(fd);
2648
+ }
2649
+ }
2650
+ }
2651
+ function formatBytes(bytes) {
2652
+ if (bytes < 1024)
2653
+ return `${bytes} B`;
2654
+ if (bytes < 1024 * 1024)
2655
+ return `${(bytes / 1024).toFixed(1)} KB`;
2656
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2657
+ }
2658
+
2659
+ // packages/cli/dist/commands/migrate.js
2660
+ import fs11 from "node:fs";
2661
+ import path9 from "node:path";
2662
+ var USAGE = `teamos migrate --from <legacy-home> [--to <new-home>] [flags]
2663
+
2664
+ Import a legacy teamwork-os home (jobs + settings) into the rebuilt stack.
2665
+ Prints the full plan and writes nothing unless --apply is given.
2666
+
2667
+ --from <dir> Legacy $TEAMWORK_HOME to read (required)
2668
+ --to <dir> Target home to write (default: $TEAMWORK_HOME, then ~/.teamwork)
2669
+ --apply Write the plan. Without it this is a dry run.
2670
+ --force Apply even though the target already holds job files.
2671
+ --pause-all Import every job disabled \u2014 the parallel-run posture.
2672
+ `;
2673
+ function serialize(value) {
2674
+ return `${JSON.stringify(value, null, 2)}
2675
+ `;
2676
+ }
2677
+ function serializeBare(value) {
2678
+ return JSON.stringify(value, null, 2);
2679
+ }
2680
+ function readJsonFile(file) {
2681
+ try {
2682
+ return { ok: true, value: JSON.parse(fs11.readFileSync(file, "utf8")) };
2683
+ } catch (e) {
2684
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
2685
+ }
2686
+ }
2687
+ function readLegacyJobs(legacyHome) {
2688
+ const dir = path9.join(legacyHome, "jobs");
2689
+ let names;
2690
+ try {
2691
+ names = fs11.readdirSync(dir).filter((n) => n.endsWith(".json")).sort();
2692
+ } catch (e) {
2693
+ return { ok: false, error: `cannot read ${dir}: ${e instanceof Error ? e.message : String(e)}` };
2694
+ }
2695
+ const jobs = [];
2696
+ const unreadable = [];
2697
+ for (const name of names) {
2698
+ const full = path9.join(dir, name);
2699
+ let entry;
2700
+ try {
2701
+ entry = fs11.lstatSync(full);
2702
+ } catch (e) {
2703
+ unreadable.push({ file: `jobs/${name}`, reason: `cannot stat \u2014 ${e instanceof Error ? e.message : String(e)}` });
2704
+ continue;
2705
+ }
2706
+ if (!entry.isFile()) {
2707
+ unreadable.push({
2708
+ file: `jobs/${name}`,
2709
+ reason: entry.isSymbolicLink() ? "a symlink, not a regular file \u2014 it would import content from outside --from" : "not a regular file"
2710
+ });
2711
+ continue;
2712
+ }
2713
+ const read = readJsonFile(full);
2714
+ if (!read.ok || !read.value || typeof read.value !== "object" || Array.isArray(read.value)) {
2715
+ unreadable.push({ file: `jobs/${name}`, reason: read.ok ? "not a JSON object" : `unreadable \u2014 ${read.error}` });
2716
+ continue;
2717
+ }
2718
+ jobs.push({ file: `jobs/${name}`, raw: read.value });
2719
+ }
2720
+ return { ok: true, jobs, unreadable };
2721
+ }
2722
+ function mergeTargetSettings(parsed) {
2723
+ const base = structuredClone(DEFAULT_SETTINGS);
2724
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
2725
+ return base;
2726
+ for (const [section, value] of Object.entries(parsed)) {
2727
+ if (!(section in base))
2728
+ continue;
2729
+ if (!value || typeof value !== "object" || Array.isArray(value))
2730
+ continue;
2731
+ base[section] = { ...base[section], ...value };
2732
+ }
2733
+ return base;
2734
+ }
2735
+ function listExisting(dir, suffix = ".json") {
2736
+ try {
2737
+ return fs11.readdirSync(dir).filter((n) => n.endsWith(suffix)).map((n) => n.slice(0, -suffix.length)).sort();
2738
+ } catch {
2739
+ return [];
2740
+ }
2741
+ }
2742
+ function buildWrites(plan, targetHome) {
2743
+ const writes = [];
2744
+ for (const job of plan.jobs) {
2745
+ writes.push({
2746
+ path: path9.join(targetHome, "jobs", `${job.id}.json`),
2747
+ contents: serialize(job.job),
2748
+ mode: PROMPT_BEARING_MODE
2749
+ });
2750
+ }
2751
+ writes.push({ path: path9.join(targetHome, "results", "settings.json"), contents: serializeBare(plan.settings.settings) });
2752
+ if (plan.pauseEntries.length > 0) {
2753
+ writes.push({
2754
+ path: path9.join(targetHome, "results", "scheduler-state.json"),
2755
+ contents: serializeBare({ entries: plan.pauseEntries })
2756
+ });
2757
+ }
2758
+ return writes;
2759
+ }
2760
+ function outcomeFor(write) {
2761
+ try {
2762
+ return fs11.readFileSync(write.path, "utf8") === write.contents ? "unchanged" : "updated";
2763
+ } catch {
2764
+ return "created";
2765
+ }
2766
+ }
2767
+ function writeAtomic(file, contents, mode) {
2768
+ fs11.mkdirSync(path9.dirname(file), { recursive: true, mode: 448 });
2769
+ const tmp = `${file}.tmp.${process.pid}.${Date.now()}`;
2770
+ fs11.writeFileSync(tmp, contents, mode !== void 0 ? { mode } : void 0);
2771
+ fs11.renameSync(tmp, file);
2772
+ }
2773
+ var PROMPT_BEARING_MODE = 384;
2774
+ async function migrateCommand(ctx) {
2775
+ const { args, out, err } = ctx;
2776
+ if (ctx.rest[0] === "help" || boolFlag(args, "help")) {
2777
+ out(USAGE);
2778
+ return EXIT_OK;
2779
+ }
2780
+ const from = stringFlag(args, "from");
2781
+ if (!from) {
2782
+ err("teamos migrate: --from <legacy-home> is required\n");
2783
+ err(USAGE);
2784
+ return EXIT_USAGE;
2785
+ }
2786
+ const legacyHome = path9.resolve(from);
2787
+ const targetHome = path9.resolve(stringFlag(args, "to") ?? teamworkHomeDir());
2788
+ if (legacyHome === targetHome) {
2789
+ err("teamos migrate: --from and --to are the same directory; an import must have two homes");
2790
+ return EXIT_USAGE;
2791
+ }
2792
+ const apply = boolFlag(args, "apply");
2793
+ const force = boolFlag(args, "force");
2794
+ const pauseAll = boolFlag(args, "pause-all");
2795
+ const legacyJobs = readLegacyJobs(legacyHome);
2796
+ if (!legacyJobs.ok) {
2797
+ err(`teamos migrate: ${legacyJobs.error}`);
2798
+ return EXIT_FAILURE;
2799
+ }
2800
+ for (const problem of legacyJobs.unreadable)
2801
+ err(`teamos migrate: skipping ${problem.file}: ${problem.reason}`);
2802
+ const legacySettingsPath = path9.join(legacyHome, "results", "settings.json");
2803
+ let legacySettings = null;
2804
+ if (fs11.existsSync(legacySettingsPath)) {
2805
+ const read = readJsonFile(legacySettingsPath);
2806
+ if (!read.ok) {
2807
+ err(`teamos migrate: cannot parse ${legacySettingsPath}: ${read.error}`);
2808
+ return EXIT_FAILURE;
2809
+ }
2810
+ if (read.value && typeof read.value === "object" && !Array.isArray(read.value)) {
2811
+ legacySettings = read.value;
2812
+ } else {
2813
+ err(`teamos migrate: ${legacySettingsPath} is not a JSON object; settings are not imported`);
2814
+ }
2815
+ } else {
2816
+ err(`teamos migrate: no ${legacySettingsPath}; settings are not imported`);
2817
+ }
2818
+ if (legacyJobs.jobs.length === 0 && legacySettings === null) {
2819
+ err(`teamos migrate: nothing to import from ${legacyHome}` + (legacyJobs.unreadable.length > 0 ? ` \u2014 ${legacyJobs.unreadable.length} job file(s) were unreadable (listed above) and there are no settings` : ""));
2820
+ return EXIT_FAILURE;
2821
+ }
2822
+ const targetSettingsRead = readJsonFile(path9.join(targetHome, "results", "settings.json"));
2823
+ const targetSettings = mergeTargetSettings(targetSettingsRead.ok ? targetSettingsRead.value : null);
2824
+ const existingJobIds = listExisting(path9.join(targetHome, "jobs"));
2825
+ const plan = planMigration({
2826
+ jobs: legacyJobs.jobs,
2827
+ unreadable: legacyJobs.unreadable,
2828
+ settings: legacySettings,
2829
+ target: { existingJobIds, settings: targetSettings },
2830
+ pauseAll,
2831
+ now: (/* @__PURE__ */ new Date()).toISOString()
2832
+ });
2833
+ if (!apply) {
2834
+ out(renderPlan(plan, { from: legacyHome, to: targetHome }));
2835
+ if (existingJobIds.length > 0) {
2836
+ out("");
2837
+ out(`note: ${existingJobIds.length} job file(s) already in the target home \u2014 an --apply will need --force unless the plan matches them exactly.`);
2838
+ }
2839
+ return EXIT_OK;
2840
+ }
2841
+ const writes = buildWrites(plan, targetHome);
2842
+ const outcomes = writes.map((w) => ({ path: w.path, outcome: outcomeFor(w) }));
2843
+ const noop = outcomes.every((o) => o.outcome === "unchanged");
2844
+ if (!force && !noop && existingJobIds.length > 0) {
2845
+ err(`teamos migrate: ${targetHome} already holds ${existingJobIds.length} job file(s) (${existingJobIds.slice(0, 5).join(", ")}${existingJobIds.length > 5 ? ", \u2026" : ""}).`);
2846
+ err("Re-run with --force to overwrite them, or point --to at an empty home.");
2847
+ return EXIT_FAILURE;
2848
+ }
2849
+ if (noop) {
2850
+ out(`teamos migrate: no changes \u2014 ${targetHome} already matches this plan (${writes.length} file(s) verified).`);
2851
+ } else {
2852
+ for (const write of writes)
2853
+ writeAtomic(write.path, write.contents, write.mode);
2854
+ }
2855
+ const reportPath = path9.join(targetHome, "migration-report.md");
2856
+ const report = renderReport(plan, { from: legacyHome, to: targetHome, applied: { files: outcomes, noop }, force });
2857
+ writeAtomic(reportPath, report);
2858
+ if (!noop) {
2859
+ const created = outcomes.filter((o) => o.outcome === "created").length;
2860
+ const updated = outcomes.filter((o) => o.outcome === "updated").length;
2861
+ out(`teamos migrate: applied to ${targetHome} \u2014 ${plan.jobs.length} job(s), ${created} file(s) created, ${updated} updated.`);
2862
+ if (plan.rejected.length > 0)
2863
+ out(` ${plan.rejected.length} legacy job(s) were NOT imported \u2014 see the report.`);
2864
+ if (plan.pauseAll)
2865
+ out(" every job was imported disabled (--pause-all).");
2866
+ }
2867
+ out(`Report: ${reportPath}`);
2868
+ return EXIT_OK;
2869
+ }
2870
+
2871
+ // packages/cli/dist/commands/model.js
2872
+ var SIBLING_FAMILY_PREFIXES = {
2873
+ "claude-fable-": ["claude-mythos-"],
2874
+ "claude-mythos-": ["claude-fable-"]
2875
+ };
2876
+ function familyPrefixOf(model) {
2877
+ return /^(claude-[a-z]+-)/.exec(model)?.[1] ?? null;
2878
+ }
2879
+ function rungFamilyPrefixes(table2) {
2880
+ const out = {};
2881
+ for (const rung of MODEL_ALIASES) {
2882
+ const prefix = familyPrefixOf(table2[rung].model);
2883
+ out[rung] = prefix ? [prefix, ...SIBLING_FAMILY_PREFIXES[prefix] ?? []] : [];
2884
+ }
2885
+ return out;
2886
+ }
2887
+ function familyLabel(prefix) {
2888
+ return prefix.replace(/^claude-/, "").replace(/-$/, "");
2889
+ }
2890
+ function parseCreatedAt(value) {
2891
+ const ms = new Date(value).getTime();
2892
+ return Number.isNaN(ms) ? null : ms;
2893
+ }
2894
+ function detectModelUpdates(apiModels, table2 = DEFAULT_MODEL_ALIASES) {
2895
+ const byId = new Map(apiModels.map((m) => [m.id, m]));
2896
+ const rungPrefixes = rungFamilyPrefixes(table2);
2897
+ const allKnownPrefixes = Array.from(new Set(Object.values(rungPrefixes).flat()));
2898
+ const candidates = [];
2899
+ let newestKnownMs = null;
2900
+ for (const rung of MODEL_ALIASES) {
2901
+ const currentModel = table2[rung].model;
2902
+ const currentEntry = byId.get(currentModel);
2903
+ if (!currentEntry)
2904
+ continue;
2905
+ const currentCreatedMs = parseCreatedAt(currentEntry.created_at);
2906
+ if (currentCreatedMs === null)
2907
+ continue;
2908
+ newestKnownMs = newestKnownMs === null ? currentCreatedMs : Math.max(newestKnownMs, currentCreatedMs);
2909
+ const prefixes = rungPrefixes[rung];
2910
+ if (prefixes.length === 0)
2911
+ continue;
2912
+ const newer = apiModels.filter((m) => m.id !== currentModel && prefixes.some((p) => m.id.startsWith(p))).map((m) => ({ model: m, createdMs: parseCreatedAt(m.created_at) })).filter((entry) => entry.createdMs !== null).filter((entry) => entry.createdMs > currentCreatedMs).sort((a, b) => b.createdMs - a.createdMs);
2913
+ const best = newer[0];
2914
+ if (!best)
2915
+ continue;
2916
+ const matchedPrefix = prefixes.find((p) => best.model.id.startsWith(p)) ?? prefixes[0];
2917
+ candidates.push({
2918
+ rung,
2919
+ family: familyLabel(matchedPrefix),
2920
+ currentModel,
2921
+ currentCreatedAt: currentEntry.created_at,
2922
+ newModel: best.model.id,
2923
+ newModelDisplayName: best.model.display_name,
2924
+ newCreatedAt: best.model.created_at
2925
+ });
2926
+ }
2927
+ const floorMs = newestKnownMs;
2928
+ const unrecognized = floorMs === null ? [] : apiModels.filter((m) => !allKnownPrefixes.some((p) => m.id.startsWith(p))).filter((m) => {
2929
+ const ms = parseCreatedAt(m.created_at);
2930
+ return ms !== null && ms > floorMs;
2931
+ }).map((m) => ({ id: m.id, display_name: m.display_name, created_at: m.created_at }));
2932
+ return { candidates, unrecognized };
2933
+ }
2934
+ async function fetchAllAnthropicModels(opts) {
2935
+ const fetchFn = opts.fetchImpl ?? fetch;
2936
+ const baseUrl = opts.baseUrl ?? "https://api.anthropic.com/v1/models";
2937
+ const models = [];
2938
+ let afterId;
2939
+ let pageIndex = 0;
2940
+ for (; ; ) {
2941
+ const url = new URL(baseUrl);
2942
+ if (afterId)
2943
+ url.searchParams.set("after_id", afterId);
2944
+ const urlStr = url.toString();
2945
+ const res = await fetchFn(urlStr, {
2946
+ headers: {
2947
+ "x-api-key": opts.apiKey,
2948
+ "anthropic-version": "2023-06-01"
2949
+ }
2950
+ });
2951
+ if (!res.ok) {
2952
+ throw new Error(`Anthropic /v1/models request failed on page ${pageIndex} (${urlStr}): ${res.status} ${res.statusText}`);
2953
+ }
2954
+ let page;
2955
+ try {
2956
+ page = await res.json();
2957
+ } catch (err) {
2958
+ throw new Error(`Anthropic /v1/models returned an unparseable body on page ${pageIndex} (${urlStr}): ${err instanceof Error ? err.message : String(err)}`);
2959
+ }
2960
+ models.push(...page.data);
2961
+ if (!page.has_more)
2962
+ break;
2963
+ if (!page.last_id) {
2964
+ throw new Error(`Anthropic /v1/models page ${pageIndex} (${urlStr}) reported has_more=true but no last_id to continue from \u2014 refusing to silently truncate the catalog.`);
2965
+ }
2966
+ afterId = page.last_id;
2967
+ pageIndex += 1;
2968
+ }
2969
+ return models;
2970
+ }
2971
+ var MODEL_MONITOR_API_KEY_ENV_VAR = "MODEL_MONITOR_ANTHROPIC_API_KEY";
2972
+ async function checkForModelUpdates(opts) {
2973
+ const apiKey = opts?.apiKey ?? process.env["MODEL_MONITOR_ANTHROPIC_API_KEY"];
2974
+ if (!apiKey) {
2975
+ throw new Error(`${MODEL_MONITOR_API_KEY_ENV_VAR} is not set in the job environment \u2014 cannot query the Anthropic Models API.`);
2976
+ }
2977
+ const apiModels = await fetchAllAnthropicModels({ apiKey, fetchImpl: opts?.fetchImpl });
2978
+ return detectModelUpdates(apiModels, DEFAULT_MODEL_ALIASES);
2979
+ }
2980
+ var USAGE2 = `teamos model <subcommand>
2981
+
2982
+ Inspect the capability-alias model ladder (packages/shared/src/model-aliases.ts).
2983
+
2984
+ check-updates Query the Anthropic Models API for same-family models newer
2985
+ than the current ladder, plus any unrecognized new families.
2986
+ Prints a JSON report; see docs/model-adoption-playbook.md for
2987
+ what to do with it. Needs ${MODEL_MONITOR_API_KEY_ENV_VAR} in
2988
+ the environment.
2989
+ `;
2990
+ async function modelCommand(ctx) {
2991
+ const [sub] = ctx.rest;
2992
+ switch (sub) {
2993
+ case "check-updates": {
2994
+ let report;
2995
+ try {
2996
+ report = await checkForModelUpdates();
2997
+ } catch (err) {
2998
+ ctx.err(`teamos model check-updates: ${err instanceof Error ? err.message : String(err)}`);
2999
+ return EXIT_FAILURE;
3000
+ }
3001
+ ctx.out(JSON.stringify(report, null, 2));
3002
+ return EXIT_OK;
3003
+ }
3004
+ case void 0:
3005
+ case "help":
3006
+ ctx.out(USAGE2);
3007
+ return EXIT_OK;
3008
+ default:
3009
+ ctx.err(`Unknown model subcommand: ${sub}
3010
+ Usage: teamos model <check-updates>`);
3011
+ return EXIT_USAGE;
3012
+ }
3013
+ }
3014
+
3015
+ // packages/cli/dist/commands/sessions.js
3016
+ var VALID_STATUSES = /* @__PURE__ */ new Set(["idle", "running", "error", "waiting", "interrupted"]);
3017
+ async function sessionsCommand(ctx) {
3018
+ const [sub, ...rest] = ctx.rest;
3019
+ switch (sub) {
3020
+ case void 0:
3021
+ case "list": {
3022
+ const limit = intFlag(ctx.args, "limit", { min: 1, max: 200 });
3023
+ if (limit !== void 0 && typeof limit === "object") {
3024
+ ctx.err(`teamos: ${limit.error}`);
3025
+ return EXIT_USAGE;
3026
+ }
3027
+ const status = stringFlag(ctx.args, "status");
3028
+ if (status !== void 0 && !VALID_STATUSES.has(status)) {
3029
+ ctx.err(`teamos: --status must be one of ${[...VALID_STATUSES].join(", ")} (got ${JSON.stringify(status)})`);
3030
+ return EXIT_USAGE;
3031
+ }
3032
+ const params = new URLSearchParams();
3033
+ params.set("limit", String(limit ?? 25));
3034
+ if (status)
3035
+ params.set("status", status);
3036
+ const res = await ctx.client.get(`/api/sessions?${params.toString()}`);
3037
+ if (!res.ok) {
3038
+ ctx.err(describeFailure(res, ctx.client.base));
3039
+ return EXIT_FAILURE;
3040
+ }
3041
+ const body = res.data ?? {};
3042
+ const sessions = body.sessions ?? [];
3043
+ if (sessions.length === 0) {
3044
+ ctx.out("No sessions.");
3045
+ return EXIT_OK;
3046
+ }
3047
+ ctx.out(table(["ID", "STATUS", "UPDATED", "CWD"], sessions.map((s) => [
3048
+ s.id ? truncate(s.id, 24) : "\u2014",
3049
+ s.status,
3050
+ relativeTime(s.updatedAt ?? s.createdAt),
3051
+ s.cwd ? truncate(s.cwd, 48) : "\u2014"
3052
+ ])));
3053
+ if (typeof body.total === "number" && body.total > sessions.length) {
3054
+ ctx.out(`
3055
+ Showing ${sessions.length} of ${body.total}. Raise the window with --limit.`);
3056
+ }
3057
+ return EXIT_OK;
3058
+ }
3059
+ case "show": {
3060
+ const id = rest[0];
3061
+ if (!id) {
3062
+ ctx.err("Usage: teamos sessions show <id>");
3063
+ return EXIT_USAGE;
3064
+ }
3065
+ const res = await ctx.client.get(`/api/sessions/${encodeURIComponent(id)}`);
3066
+ if (!res.ok) {
3067
+ ctx.err(describeFailure(res, ctx.client.base));
3068
+ return EXIT_FAILURE;
3069
+ }
3070
+ ctx.out(JSON.stringify(res.data, null, 2));
3071
+ return EXIT_OK;
3072
+ }
3073
+ case "stop": {
3074
+ const id = rest[0];
3075
+ if (!id) {
3076
+ ctx.err("Usage: teamos sessions stop <id>");
3077
+ return EXIT_USAGE;
3078
+ }
3079
+ const res = await ctx.client.post(`/api/sessions/${encodeURIComponent(id)}/stop`);
3080
+ if (!res.ok) {
3081
+ ctx.err(describeFailure(res, ctx.client.base));
3082
+ return EXIT_FAILURE;
3083
+ }
3084
+ ctx.out(`Interrupted session ${id} and dropped its queued turns.`);
3085
+ return EXIT_OK;
3086
+ }
3087
+ default:
3088
+ ctx.err(`Unknown sessions subcommand: ${sub}
3089
+ Usage: teamos sessions <list|show|stop>`);
3090
+ return EXIT_USAGE;
3091
+ }
3092
+ }
3093
+
3094
+ // packages/cli/dist/commands/settings.js
3095
+ function buildPatch(dottedPath, value) {
3096
+ const segments = dottedPath.split(".").filter((s) => s.length > 0);
3097
+ if (segments.length === 0)
3098
+ throw new Error("empty settings path");
3099
+ const root = {};
3100
+ let cursor = root;
3101
+ for (let i = 0; i < segments.length - 1; i++) {
3102
+ const next = {};
3103
+ cursor[segments[i]] = next;
3104
+ cursor = next;
3105
+ }
3106
+ cursor[segments[segments.length - 1]] = value;
3107
+ return root;
3108
+ }
3109
+ function readPath(source, dottedPath) {
3110
+ const segments = dottedPath.split(".").filter((s) => s.length > 0);
3111
+ let cursor = source;
3112
+ for (const seg of segments) {
3113
+ if (cursor === null || typeof cursor !== "object")
3114
+ return void 0;
3115
+ cursor = cursor[seg];
3116
+ }
3117
+ return cursor;
3118
+ }
3119
+ function coerceValue(raw) {
3120
+ const trimmed = raw.trim();
3121
+ if (trimmed === "true")
3122
+ return true;
3123
+ if (trimmed === "false")
3124
+ return false;
3125
+ if (trimmed === "null")
3126
+ return null;
3127
+ if (/^-?\d+(\.\d+)?$/.test(trimmed))
3128
+ return Number(trimmed);
3129
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
3130
+ try {
3131
+ return JSON.parse(trimmed);
3132
+ } catch {
3133
+ }
3134
+ }
3135
+ return raw;
3136
+ }
3137
+ async function settingsCommand(ctx) {
3138
+ const [sub, ...rest] = ctx.rest;
3139
+ switch (sub) {
3140
+ case void 0:
3141
+ case "get": {
3142
+ const res = await ctx.client.get("/api/settings");
3143
+ if (!res.ok) {
3144
+ ctx.err(describeFailure(res, ctx.client.base));
3145
+ return EXIT_FAILURE;
3146
+ }
3147
+ const path11 = rest[0];
3148
+ if (!path11) {
3149
+ ctx.out(JSON.stringify(res.data, null, 2));
3150
+ return EXIT_OK;
3151
+ }
3152
+ const value = readPath(res.data, path11);
3153
+ if (value === void 0) {
3154
+ ctx.err(`teamos: no settings value at ${path11}`);
3155
+ return EXIT_FAILURE;
3156
+ }
3157
+ ctx.out(typeof value === "string" ? value : JSON.stringify(value, null, 2));
3158
+ return EXIT_OK;
3159
+ }
3160
+ case "set": {
3161
+ const path11 = rest[0];
3162
+ const raw = rest[1];
3163
+ if (!path11 || raw === void 0) {
3164
+ ctx.err("Usage: teamos settings set <dotted.path> <value>");
3165
+ return EXIT_USAGE;
3166
+ }
3167
+ if (!path11.includes(".")) {
3168
+ ctx.err(`teamos: settings paths are dotted, e.g. retention.days or server.bindHost (got ${JSON.stringify(path11)})`);
3169
+ return EXIT_USAGE;
3170
+ }
3171
+ const patch = buildPatch(path11, coerceValue(raw));
3172
+ const res = await ctx.client.put("/api/settings", patch);
3173
+ if (!res.ok) {
3174
+ ctx.err(describeFailure(res, ctx.client.base));
3175
+ return EXIT_FAILURE;
3176
+ }
3177
+ const stored = readPath(res.data, path11);
3178
+ ctx.out(`${path11} = ${JSON.stringify(stored)}`);
3179
+ return EXIT_OK;
3180
+ }
3181
+ default:
3182
+ ctx.err(`Unknown settings subcommand: ${sub}
3183
+ Usage: teamos settings <get|set>`);
3184
+ return EXIT_USAGE;
3185
+ }
3186
+ }
3187
+
3188
+ // packages/cli/dist/version.js
3189
+ import fs12 from "node:fs";
3190
+ import path10 from "node:path";
3191
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
3192
+ function cliVersion() {
3193
+ const dir = path10.dirname(fileURLToPath2(import.meta.url));
3194
+ try {
3195
+ const parsed = JSON.parse(fs12.readFileSync(path10.join(dir, "..", "package.json"), "utf8"));
3196
+ return typeof parsed.version === "string" ? parsed.version : "unknown";
3197
+ } catch {
3198
+ return "unknown";
3199
+ }
3200
+ }
3201
+
3202
+ // packages/cli/dist/commands/status.js
3203
+ async function healthCommand(ctx) {
3204
+ const res = await ctx.client.get("/api/health");
3205
+ if (!res.ok) {
3206
+ ctx.err(describeFailure(res, ctx.client.base));
3207
+ return EXIT_FAILURE;
3208
+ }
3209
+ ctx.out(JSON.stringify(res.data, null, 2));
3210
+ return EXIT_OK;
3211
+ }
3212
+ function launchdLine(label, port, configured) {
3213
+ if (label === null)
3214
+ return "not managed (started manually)";
3215
+ if (port === configured)
3216
+ return label;
3217
+ return `${label} \u2014 host-level, and it manages :${configured}, not the :${port} queried here`;
3218
+ }
3219
+ async function statusCommand(ctx) {
3220
+ const configured = configuredPort(process.env, () => {
3221
+ });
3222
+ const health = await ctx.client.get("/api/health");
3223
+ if (!health.ok) {
3224
+ ctx.out("Gateway: stopped");
3225
+ ctx.out(` Port: ${ctx.port}`);
3226
+ ctx.out(` Home: ${teamworkHomeDir()}`);
3227
+ const label2 = await findManagedLabel(void 0, ctx.err);
3228
+ if (label2) {
3229
+ ctx.out(ctx.port === configured ? ` launchd: ${label2} is loaded but the gateway is not answering \u2014 check the launchd error log` : ` launchd: ${launchdLine(label2, ctx.port, configured)}`);
3230
+ }
3231
+ ctx.err(describeFailure(health, ctx.client.base));
3232
+ return EXIT_FAILURE;
3233
+ }
3234
+ const body = health.data ?? {};
3235
+ ctx.out("Gateway: running");
3236
+ const label = await findManagedLabel(void 0, ctx.err);
3237
+ ctx.out(keyValues([
3238
+ [" Port", ctx.port],
3239
+ [" Version", body.version],
3240
+ [" Commit", body.commit ? body.commit.slice(0, 7) : null],
3241
+ [" Uptime", body.uptime === void 0 ? null : formatUptime(body.uptime)],
3242
+ [" Home", teamworkHomeDir()],
3243
+ [" launchd", launchdLine(label, ctx.port, configured)]
3244
+ ]));
3245
+ const [jobs, sessions, scheduler] = await Promise.all([
3246
+ ctx.client.get("/api/jobs"),
3247
+ ctx.client.get("/api/sessions?limit=1"),
3248
+ ctx.client.get("/api/scheduler")
3249
+ ]);
3250
+ if (jobs.ok && Array.isArray(jobs.data)) {
3251
+ const list = jobs.data;
3252
+ const enabled = list.filter((j) => j.enabled).length;
3253
+ const running = list.filter((j) => j.lastStatus === "running" || j.lastStatus === "retrying").length;
3254
+ const lastRun = list.map((j) => j.lastRun).filter((v) => typeof v === "string").sort().at(-1);
3255
+ ctx.out(` Jobs: ${list.length} total, ${enabled} enabled, ${running} running (last run ${relativeTime(lastRun)})`);
3256
+ } else if (jobs.status !== 0) {
3257
+ ctx.out(` Jobs: unavailable (${describeFailure(jobs, ctx.client.base)})`);
3258
+ }
3259
+ if (sessions.ok && sessions.data !== null && typeof sessions.data === "object") {
3260
+ const total = sessions.data.total;
3261
+ ctx.out(` Sessions: ${total ?? "unknown"} total`);
3262
+ }
3263
+ if (scheduler.ok && scheduler.data !== null && typeof scheduler.data === "object") {
3264
+ const s = scheduler.data;
3265
+ const parts = [s.paused ? "PAUSED" : "active"];
3266
+ if (typeof s.inFlight === "number")
3267
+ parts.push(`${s.inFlight} in flight`);
3268
+ if (typeof s.queued === "number")
3269
+ parts.push(`${s.queued} queued`);
3270
+ ctx.out(` Scheduler: ${parts.join(", ")}`);
3271
+ }
3272
+ return EXIT_OK;
3273
+ }
3274
+ function formatUptime(seconds) {
3275
+ const s = Math.floor(seconds);
3276
+ if (s < 60)
3277
+ return `${s}s`;
3278
+ if (s < 3600)
3279
+ return `${Math.floor(s / 60)}m ${s % 60}s`;
3280
+ if (s < 86400)
3281
+ return `${Math.floor(s / 3600)}h ${Math.floor(s % 3600 / 60)}m`;
3282
+ return `${Math.floor(s / 86400)}d ${Math.floor(s % 86400 / 3600)}h`;
3283
+ }
3284
+
3285
+ // packages/cli/dist/cli.js
3286
+ var USAGE3 = `teamos \u2014 teamwork-os control CLI
3287
+
3288
+ Usage: teamos <command> [subcommand] [args] [--flags]
3289
+
3290
+ Gateway
3291
+ status Gateway liveness, version, launchd state, subsystem counts
3292
+ health Raw GET /api/health
3293
+ version Print the CLI version
3294
+
3295
+ Lifecycle (launchd-aware on macOS)
3296
+ start Start the gateway (kickstart when launchd manages it)
3297
+ stop Stop the gateway
3298
+ restart Restart the gateway
3299
+ install Render + load the gateway and watchdog LaunchAgents
3300
+ uninstall Unload and remove them
3301
+ deploy Fetch, fast-forward, install, build, restart
3302
+
3303
+ Configuration
3304
+ env list|get|set|unset|audit|normalize
3305
+ settings get [path] | settings set <dotted.path> <value>
3306
+ model check-updates
3307
+
3308
+ Work
3309
+ jobs list|show|create|update|delete|run|pause|resume|runs|trace
3310
+ sessions list|show|stop
3311
+ logs [name] [--lines N] [--follow]
3312
+
3313
+ Migration
3314
+ migrate --from <legacy-home> [--to <home>] [--apply] [--force] [--pause-all]
3315
+
3316
+ Common flags
3317
+ --port N Gateway port (default: PORT, then the teamwork env file, then 7463)
3318
+ --help This text
3319
+
3320
+ Exit codes: 0 success, 1 the operation failed, 2 the invocation was wrong.
3321
+ `;
3322
+ async function run2(argv, out = console.log, err = console.error) {
3323
+ const args = parseArgs(argv);
3324
+ const [command, ...rest] = args.positionals;
3325
+ if (command === void 0) {
3326
+ err(USAGE3);
3327
+ return EXIT_USAGE;
3328
+ }
3329
+ if (command === "help" || command === "--help" || args.flags.help === true) {
3330
+ out(USAGE3);
3331
+ return EXIT_OK;
3332
+ }
3333
+ if (command === "version" || command === "--version" || command === "-v") {
3334
+ out(cliVersion());
3335
+ return EXIT_OK;
3336
+ }
3337
+ const port = resolvePort(args, process.env, err);
3338
+ if (typeof port === "object") {
3339
+ err(`teamos: ${port.error}`);
3340
+ return EXIT_USAGE;
3341
+ }
3342
+ const ctx = { args, rest, port, client: makeClient(port, err), out, err };
3343
+ try {
3344
+ switch (command) {
3345
+ case "status":
3346
+ return await statusCommand(ctx);
3347
+ case "health":
3348
+ return await healthCommand(ctx);
3349
+ case "env":
3350
+ return await envCommand(ctx);
3351
+ case "settings":
3352
+ return await settingsCommand(ctx);
3353
+ case "model":
3354
+ return await modelCommand(ctx);
3355
+ case "jobs":
3356
+ return await jobsCommand(ctx);
3357
+ case "sessions":
3358
+ return await sessionsCommand(ctx);
3359
+ case "logs":
3360
+ return await logsCommand(ctx);
3361
+ case "migrate":
3362
+ return await migrateCommand(ctx);
3363
+ case "start":
3364
+ return await startCommand(ctx);
3365
+ case "stop":
3366
+ return await stopCommand(ctx);
3367
+ case "restart":
3368
+ return await restartCommand(ctx);
3369
+ case "install":
3370
+ return await installCommand(ctx);
3371
+ case "uninstall":
3372
+ return await uninstallCommand(ctx);
3373
+ case "deploy":
3374
+ return await deployCommand(ctx);
3375
+ default:
3376
+ err(`Unknown command: ${command}
3377
+ `);
3378
+ err(USAGE3);
3379
+ return EXIT_USAGE;
3380
+ }
3381
+ } catch (e) {
3382
+ err(`teamos: ${e instanceof Error ? e.message : String(e)}`);
3383
+ return EXIT_FAILURE;
3384
+ }
3385
+ }
3386
+
3387
+ // packages/cli/dist/index.js
3388
+ var code = await run2(process.argv.slice(2));
3389
+ process.exitCode = code;