@todos-dev/cli 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +414 -175
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -484,6 +484,11 @@ function text(s, details) {
|
|
|
484
484
|
// src/lib/todoTools.ts
|
|
485
485
|
var REQUEST_TIMEOUT_MS = 1e4;
|
|
486
486
|
var LIST_CAP = 8e3;
|
|
487
|
+
function toolScope(step) {
|
|
488
|
+
if (step.workspace?.projectId) return { kind: "project", projectId: step.workspace.projectId };
|
|
489
|
+
if (step.chiefId) return { kind: "account", chiefId: step.chiefId };
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
487
492
|
async function request(serverUrl, method, path, token, body) {
|
|
488
493
|
const headers = { "Content-Type": "application/json" };
|
|
489
494
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
@@ -511,60 +516,80 @@ function makeTodoTools(serverUrl, token, step) {
|
|
|
511
516
|
];
|
|
512
517
|
}
|
|
513
518
|
function makeListTodosTool(serverUrl, token, step) {
|
|
514
|
-
const
|
|
519
|
+
const scope = toolScope(step);
|
|
515
520
|
return {
|
|
516
521
|
name: "list_todos",
|
|
517
522
|
label: "List Todos",
|
|
518
|
-
description: `List the todos in this project so you can see existing/related work and find a todo to update. The first line lists the project's tags with their ids (usable as update_todo's tagIds). Returns each todo as "#<seq> [<phase>] <title> (id: <id>) [tags: \u2026]"; todos you created are marked (yours).`,
|
|
523
|
+
description: scope?.kind === "account" ? `List the todo boards of every project across the user's teams, grouped by project. Each project header carries its projectId (required by create_todo); each todo renders as "#<seq> [<phase>] <title> (id: <id>)".` : `List the todos in this project so you can see existing/related work and find a todo to update. The first line lists the project's tags with their ids (usable as update_todo's tagIds). Returns each todo as "#<seq> [<phase>] <title> (id: <id>) [tags: \u2026]"; todos you created are marked (yours).`,
|
|
519
524
|
parameters: { type: "object", properties: {}, required: [], additionalProperties: false },
|
|
520
525
|
async execute() {
|
|
521
|
-
if (
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
526
|
+
if (scope?.kind === "project") {
|
|
527
|
+
const { projectId } = scope;
|
|
528
|
+
const [reply, tagsReply] = await Promise.all([
|
|
529
|
+
request(serverUrl, "GET", `/api/projects/${projectId}/todos`, token),
|
|
530
|
+
request(serverUrl, "GET", `/api/projects/${projectId}/tags`, token)
|
|
531
|
+
]);
|
|
532
|
+
if (!reply.ok) return text2(errorText(reply, "Failed to list todos"));
|
|
533
|
+
const todos = reply.body ?? [];
|
|
534
|
+
if (!todos.length) return text2("No todos in this project yet.");
|
|
535
|
+
const tags = tagsReply.ok ? tagsReply.body ?? [] : [];
|
|
536
|
+
const tagName = new Map(tags.map((t) => [t.id, t.name]));
|
|
537
|
+
const header = tags.length ? `Project tags: ${tags.map((t) => `${t.name} (id: ${t.id})`).join(", ")}
|
|
532
538
|
` : "";
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
539
|
+
const lines = todos.map((t) => {
|
|
540
|
+
const mine = t.createdBy === step.agent.agentId ? " (yours)" : "";
|
|
541
|
+
const names = (t.tagIds ?? []).map((id) => tagName.get(id)).filter(Boolean);
|
|
542
|
+
const tagsStr = names.length ? ` [tags: ${names.join(", ")}]` : "";
|
|
543
|
+
return `#${t.seqNum} [${t.phase}] ${t.title} (id: ${t.id})${tagsStr}${mine}`;
|
|
544
|
+
});
|
|
545
|
+
let out = header + lines.join("\n");
|
|
546
|
+
if (out.length > LIST_CAP) out = out.slice(0, LIST_CAP) + "\n\u2026(truncated)";
|
|
547
|
+
return text2(out);
|
|
548
|
+
}
|
|
549
|
+
if (scope?.kind === "account") {
|
|
550
|
+
const reply = await request(serverUrl, "GET", `/api/machine/chief/todos?chiefId=${encodeURIComponent(scope.chiefId)}`, token);
|
|
551
|
+
if (!reply.ok) return text2(errorText(reply, "Failed to list todos"));
|
|
552
|
+
const projects = reply.body?.projects ?? [];
|
|
553
|
+
if (!projects.length) return text2("No projects in your teams yet.");
|
|
554
|
+
const blocks = projects.map((p) => {
|
|
555
|
+
const lines = p.todos.length ? p.todos.map((t) => `#${t.seqNum} [${t.phase}] ${t.title} (id: ${t.id})`).join("\n") : "(no todos)";
|
|
556
|
+
return `## ${p.name} (projectId: ${p.id})
|
|
557
|
+
${lines}`;
|
|
558
|
+
});
|
|
559
|
+
let out = blocks.join("\n\n");
|
|
560
|
+
if (out.length > LIST_CAP) out = out.slice(0, LIST_CAP) + "\n\u2026(truncated)";
|
|
561
|
+
return text2(out);
|
|
562
|
+
}
|
|
563
|
+
return text2("No project context for this turn \u2014 todos are unavailable.");
|
|
542
564
|
}
|
|
543
565
|
};
|
|
544
566
|
}
|
|
545
567
|
function makeCreateTodoTool(serverUrl, token, step) {
|
|
546
|
-
const
|
|
568
|
+
const scope = toolScope(step);
|
|
569
|
+
const accountScope = scope?.kind === "account";
|
|
547
570
|
return {
|
|
548
571
|
name: "create_todo",
|
|
549
572
|
label: "Create Todo",
|
|
550
|
-
description: "Create a follow-up todo for work you discovered but that is out of scope for the current task (e.g. a refactor, a separate bug, a next step). The new todo is added to the project backlog and does NOT run automatically \u2014 the user decides when to start it. Use a clear, actionable title and put the context/acceptance criteria in spec.",
|
|
573
|
+
description: accountScope ? "Create a todo in one of the user's projects (projectId comes from list_todos's project headers). The new todo lands in that project's backlog and does NOT run automatically \u2014 propose it with propose_builds when it should run. Use a clear, actionable title and put the objective, boundaries, and acceptance criteria in spec." : "Create a follow-up todo for work you discovered but that is out of scope for the current task (e.g. a refactor, a separate bug, a next step). The new todo is added to the project backlog and does NOT run automatically \u2014 the user decides when to start it. Use a clear, actionable title and put the context/acceptance criteria in spec.",
|
|
551
574
|
parameters: {
|
|
552
575
|
type: "object",
|
|
553
576
|
properties: {
|
|
554
|
-
|
|
577
|
+
...accountScope ? { projectId: { type: "string", description: "Target project id (from list_todos)." } } : {},
|
|
578
|
+
title: { type: "string", description: "Short, actionable title for the work." },
|
|
555
579
|
spec: { type: "string", description: "Details: what to do, why, and any acceptance criteria. Optional but recommended." }
|
|
556
580
|
},
|
|
557
|
-
required: ["title"],
|
|
581
|
+
required: accountScope ? ["projectId", "title"] : ["title"],
|
|
558
582
|
additionalProperties: false
|
|
559
583
|
},
|
|
560
584
|
async execute(_id, params) {
|
|
561
|
-
if (!projectId) return text2("No project context for this turn \u2014 cannot create a todo.");
|
|
562
585
|
const p = params ?? {};
|
|
586
|
+
const projectId = scope?.kind === "project" ? scope.projectId : p.projectId;
|
|
587
|
+
if (!scope || !projectId) return text2(accountScope ? "A projectId is required (find it with list_todos)." : "No project context for this turn \u2014 cannot create a todo.");
|
|
563
588
|
if (!p.title?.trim()) return text2("A title is required to create a todo.");
|
|
564
589
|
const reply = await request(serverUrl, "POST", `/api/projects/${projectId}/todos`, token, {
|
|
565
590
|
title: p.title,
|
|
566
591
|
spec: p.spec ?? "",
|
|
567
|
-
buildId: step.conversationId
|
|
592
|
+
buildId: scope.kind === "account" ? scope.chiefId : step.conversationId
|
|
568
593
|
});
|
|
569
594
|
if (!reply.ok) return text2(errorText(reply, "Failed to create todo"));
|
|
570
595
|
const t = reply.body;
|
|
@@ -573,10 +598,11 @@ function makeCreateTodoTool(serverUrl, token, step) {
|
|
|
573
598
|
};
|
|
574
599
|
}
|
|
575
600
|
function makeUpdateTodoTool(serverUrl, token, step) {
|
|
601
|
+
const scope = toolScope(step);
|
|
576
602
|
return {
|
|
577
603
|
name: "update_todo",
|
|
578
604
|
label: "Update Todo",
|
|
579
|
-
description: `Update a todo YOU created (only your own \u2014 found via list_todos, marked "(yours)"). You can change its title, spec, or tags (tag ids come from the "Project tags" line of list_todos). Use this to refine a follow-up todo as you learn more. You cannot edit todos created by others or the user, change a todo's phase, or start a build.`,
|
|
605
|
+
description: scope?.kind === "account" ? "Update a todo's title or spec \u2014 use this to groom a todo before proposing it: make the spec state the objective, boundaries, and acceptance criteria. You cannot change a todo's phase, assignment, or start a build." : `Update a todo YOU created (only your own \u2014 found via list_todos, marked "(yours)"). You can change its title, spec, or tags (tag ids come from the "Project tags" line of list_todos). Use this to refine a follow-up todo as you learn more. You cannot edit todos created by others or the user, change a todo's phase, or start a build.`,
|
|
580
606
|
parameters: {
|
|
581
607
|
type: "object",
|
|
582
608
|
properties: {
|
|
@@ -591,7 +617,7 @@ function makeUpdateTodoTool(serverUrl, token, step) {
|
|
|
591
617
|
async execute(_id, params) {
|
|
592
618
|
const p = params ?? {};
|
|
593
619
|
if (!p.id) return text2("A todo id is required (find it with list_todos).");
|
|
594
|
-
const patch = { buildId: step.conversationId };
|
|
620
|
+
const patch = { buildId: scope?.kind === "account" ? scope.chiefId : step.conversationId };
|
|
595
621
|
if (p.title !== void 0) patch.title = p.title;
|
|
596
622
|
if (p.spec !== void 0) patch.spec = p.spec;
|
|
597
623
|
if (p.tagIds !== void 0) patch.tagIds = p.tagIds;
|
|
@@ -603,6 +629,126 @@ function makeUpdateTodoTool(serverUrl, token, step) {
|
|
|
603
629
|
};
|
|
604
630
|
}
|
|
605
631
|
|
|
632
|
+
// src/lib/chiefTools.ts
|
|
633
|
+
function makeChiefTools(serverUrl, token, step) {
|
|
634
|
+
return [
|
|
635
|
+
makeProposeBuildsTool(serverUrl, token, step),
|
|
636
|
+
makeRunBuildsTool(serverUrl, token, step),
|
|
637
|
+
makeUpdateMemoryTool(serverUrl, token, step)
|
|
638
|
+
];
|
|
639
|
+
}
|
|
640
|
+
var DISPATCH_ITEMS_SCHEMA = {
|
|
641
|
+
type: "array",
|
|
642
|
+
minItems: 1,
|
|
643
|
+
items: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: {
|
|
646
|
+
todoId: { type: "string", description: "The todo to run (id from list_todos)." },
|
|
647
|
+
withPlan: { type: "boolean", description: "Start with a plan step the user confirms before implementation. Default false." },
|
|
648
|
+
agentId: { type: "string", description: "Suggested agent (from the available-agents list). Omit to use the todo's own assignment." },
|
|
649
|
+
thinkingLevel: { type: "string", description: "Suggested thinking level for the agent (only with agentId)." },
|
|
650
|
+
reason: { type: "string", description: "One line on why this todo, this agent, now." }
|
|
651
|
+
},
|
|
652
|
+
required: ["todoId"],
|
|
653
|
+
additionalProperties: false
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
function itemsToBody(items) {
|
|
657
|
+
return items.map((i) => ({
|
|
658
|
+
todoId: i.todoId,
|
|
659
|
+
...i.withPlan !== void 0 ? { withPlan: i.withPlan } : {},
|
|
660
|
+
...i.agentId ? { agent: { agentId: i.agentId, ...i.thinkingLevel ? { thinkingLevel: i.thinkingLevel } : {} } } : {},
|
|
661
|
+
...i.reason ? { reason: i.reason } : {}
|
|
662
|
+
}));
|
|
663
|
+
}
|
|
664
|
+
function makeProposeBuildsTool(serverUrl, token, step) {
|
|
665
|
+
const chiefId = step.chiefId;
|
|
666
|
+
return {
|
|
667
|
+
name: "propose_builds",
|
|
668
|
+
label: "Propose Builds",
|
|
669
|
+
description: "Propose which todos to run next. This does NOT start anything \u2014 it creates a proposal card the user reviews, adjusts, and runs. Use this for work YOU are initiating; when the user has explicitly asked to run something, use run_builds instead. Pick the agent (agentId from the available-agents list) and thinking level per item to match the work; set withPlan for tasks that deserve a reviewed plan first. Groom each todo (update_todo: objective, boundaries, acceptance criteria) BEFORE proposing it. On success, reference the returned doc id in your reply as [Run N todos](doc:<id>) so the user sees the card.",
|
|
670
|
+
parameters: {
|
|
671
|
+
type: "object",
|
|
672
|
+
properties: {
|
|
673
|
+
items: DISPATCH_ITEMS_SCHEMA,
|
|
674
|
+
summary: { type: "string", description: "One-line summary of the proposal as a whole (optional)." }
|
|
675
|
+
},
|
|
676
|
+
required: ["items"],
|
|
677
|
+
additionalProperties: false
|
|
678
|
+
},
|
|
679
|
+
async execute(_id, params) {
|
|
680
|
+
if (!chiefId) return text2("propose_builds is only available in the chief conversation.");
|
|
681
|
+
const p = params ?? {};
|
|
682
|
+
const items = (p.items ?? []).filter((i) => i?.todoId);
|
|
683
|
+
if (!items.length) return text2("At least one item with a todoId is required.");
|
|
684
|
+
const reply = await request(serverUrl, "POST", "/api/machine/chief/proposals", token, {
|
|
685
|
+
chiefId,
|
|
686
|
+
items: itemsToBody(items),
|
|
687
|
+
...p.summary ? { summary: p.summary } : {}
|
|
688
|
+
});
|
|
689
|
+
if (!reply.ok) return text2(errorText(reply, "Failed to create the proposal"));
|
|
690
|
+
const docId = reply.body?.docId;
|
|
691
|
+
return text2(`Created a proposal with ${items.length} item(s). Reference it in your reply as [Run ${items.length} todo${items.length > 1 ? "s" : ""}](doc:${docId}) so the user sees the proposal card.`);
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
function makeRunBuildsTool(serverUrl, token, step) {
|
|
696
|
+
const chiefId = step.chiefId;
|
|
697
|
+
return {
|
|
698
|
+
name: "run_builds",
|
|
699
|
+
label: "Run Builds",
|
|
700
|
+
description: "Start builds for todos IMMEDIATELY \u2014 no confirmation card. Use this ONLY when the user has explicitly asked for the work to run in this conversation (their message is the approval); for work you are initiating yourself, use propose_builds instead. Same item shape as propose_builds: pick agent/thinkingLevel per item, withPlan for tasks needing a reviewed plan. Groom each todo before running it.",
|
|
701
|
+
parameters: {
|
|
702
|
+
type: "object",
|
|
703
|
+
properties: {
|
|
704
|
+
items: DISPATCH_ITEMS_SCHEMA
|
|
705
|
+
},
|
|
706
|
+
required: ["items"],
|
|
707
|
+
additionalProperties: false
|
|
708
|
+
},
|
|
709
|
+
async execute(_id, params) {
|
|
710
|
+
if (!chiefId) return text2("run_builds is only available in the chief conversation.");
|
|
711
|
+
const p = params ?? {};
|
|
712
|
+
const items = (p.items ?? []).filter((i) => i?.todoId);
|
|
713
|
+
if (!items.length) return text2("At least one item with a todoId is required.");
|
|
714
|
+
const reply = await request(serverUrl, "POST", "/api/machine/chief/builds", token, {
|
|
715
|
+
chiefId,
|
|
716
|
+
items: itemsToBody(items)
|
|
717
|
+
});
|
|
718
|
+
if (!reply.ok) return text2(errorText(reply, "Failed to start builds"));
|
|
719
|
+
const started = reply.body?.started ?? [];
|
|
720
|
+
const skipped = items.length - started.length;
|
|
721
|
+
return text2(
|
|
722
|
+
`Started ${started.length} build(s): ${started.map((s) => `#${s.seqNum}`).join(", ")}.` + (skipped > 0 ? ` ${skipped} item(s) were skipped (phase changed or no resolvable agent).` : "") + " Tell the user what is now running and with which agents."
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
function makeUpdateMemoryTool(serverUrl, token, step) {
|
|
728
|
+
const chiefId = step.chiefId;
|
|
729
|
+
return {
|
|
730
|
+
name: "update_memory",
|
|
731
|
+
label: "Update Memory",
|
|
732
|
+
description: "Rewrite your persistent memory \u2014 the notes injected into every future turn, surviving session resets. FULL REPLACEMENT: send the complete new text, not a diff; prune stale entries to stay under the size cap. Save immediately when you learn a durable routing preference, a user preference, or a lesson \u2014 do not wait. Every update is kept as a version the user can restore.",
|
|
733
|
+
parameters: {
|
|
734
|
+
type: "object",
|
|
735
|
+
properties: {
|
|
736
|
+
content: { type: "string", description: "The complete new memory text (replaces the old entirely)." }
|
|
737
|
+
},
|
|
738
|
+
required: ["content"],
|
|
739
|
+
additionalProperties: false
|
|
740
|
+
},
|
|
741
|
+
async execute(_id, params) {
|
|
742
|
+
if (!chiefId) return text2("update_memory is only available in the chief conversation.");
|
|
743
|
+
const p = params ?? {};
|
|
744
|
+
if (!p.content?.trim()) return text2("Memory content is required (full replacement text).");
|
|
745
|
+
const reply = await request(serverUrl, "POST", "/api/machine/chief/memory", token, { chiefId, content: p.content });
|
|
746
|
+
if (!reply.ok) return text2(errorText(reply, "Failed to update memory"));
|
|
747
|
+
return text2("Memory updated. It will be injected into your future turns.");
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
606
752
|
// src/lib/sessionStore.ts
|
|
607
753
|
var import_node_fs3 = require("node:fs");
|
|
608
754
|
var import_promises = require("node:fs/promises");
|
|
@@ -1212,7 +1358,8 @@ async function executeStep(step, serverUrl, token, tunnel) {
|
|
|
1212
1358
|
const skills = await materializeSkills(step.skills, agentDir);
|
|
1213
1359
|
const extraCustomTools = [
|
|
1214
1360
|
makeClientShellTool(tunnel, conversationId, runnersRef),
|
|
1215
|
-
...makeTodoTools(serverUrl, token, step)
|
|
1361
|
+
...makeTodoTools(serverUrl, token, step),
|
|
1362
|
+
...makeChiefTools(serverUrl, token, step)
|
|
1216
1363
|
];
|
|
1217
1364
|
const session = await createSession(sdk, {
|
|
1218
1365
|
authStorage,
|
|
@@ -1367,6 +1514,18 @@ function clearConfig() {
|
|
|
1367
1514
|
}
|
|
1368
1515
|
}
|
|
1369
1516
|
|
|
1517
|
+
// src/lib/log.ts
|
|
1518
|
+
var installed = false;
|
|
1519
|
+
function installTimestampedLogging() {
|
|
1520
|
+
if (installed) return;
|
|
1521
|
+
installed = true;
|
|
1522
|
+
const stamp = () => `[${(/* @__PURE__ */ new Date()).toISOString()}]`;
|
|
1523
|
+
for (const method of ["log", "info", "warn", "error", "debug"]) {
|
|
1524
|
+
const orig = console[method].bind(console);
|
|
1525
|
+
console[method] = (...args2) => orig(stamp(), ...args2);
|
|
1526
|
+
}
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1370
1529
|
// src/lib/daemon.ts
|
|
1371
1530
|
var LOG_CAP = 10 * 1024 * 1024;
|
|
1372
1531
|
var EXIT_TERMINAL = 78;
|
|
@@ -1431,6 +1590,7 @@ function spawnSupervisor(userArgs, logFd) {
|
|
|
1431
1590
|
var MODE_FLAGS = /* @__PURE__ */ new Set(["--supervisor", "--foreground", "-f"]);
|
|
1432
1591
|
var userFlagsOnly = (args2) => args2.filter((a) => !MODE_FLAGS.has(a));
|
|
1433
1592
|
async function runSupervisor(args2) {
|
|
1593
|
+
installTimestampedLogging();
|
|
1434
1594
|
const userArgs = userFlagsOnly(args2);
|
|
1435
1595
|
const MAX_BACKOFF = 3e4;
|
|
1436
1596
|
const STABLE_MS = 6e4;
|
|
@@ -14173,14 +14333,14 @@ var safeDecodeAsync2 = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
|
|
|
14173
14333
|
var _installedGroups = /* @__PURE__ */ new WeakMap();
|
|
14174
14334
|
function _installLazyMethods(inst, group, methods) {
|
|
14175
14335
|
const proto = Object.getPrototypeOf(inst);
|
|
14176
|
-
let
|
|
14177
|
-
if (!
|
|
14178
|
-
|
|
14179
|
-
_installedGroups.set(proto,
|
|
14336
|
+
let installed2 = _installedGroups.get(proto);
|
|
14337
|
+
if (!installed2) {
|
|
14338
|
+
installed2 = /* @__PURE__ */ new Set();
|
|
14339
|
+
_installedGroups.set(proto, installed2);
|
|
14180
14340
|
}
|
|
14181
|
-
if (
|
|
14341
|
+
if (installed2.has(group))
|
|
14182
14342
|
return;
|
|
14183
|
-
|
|
14343
|
+
installed2.add(group);
|
|
14184
14344
|
for (const key in methods) {
|
|
14185
14345
|
const fn = methods[key];
|
|
14186
14346
|
Object.defineProperty(proto, key, {
|
|
@@ -16018,7 +16178,8 @@ var STEP_DEF = {
|
|
|
16018
16178
|
implement_revision: { track: "implement", sink: "chat", phase: { busy: "building", rest: "review", notify: "build_review" } },
|
|
16019
16179
|
merge: { track: "implement", sink: "chat", phase: { busy: "building", rest: "review", notify: "build_review" } },
|
|
16020
16180
|
plan_review: { track: "plan", sink: "review" },
|
|
16021
|
-
implement_review: { track: "implement", sink: "review" }
|
|
16181
|
+
implement_review: { track: "implement", sink: "review" },
|
|
16182
|
+
chief: { sink: "chief" }
|
|
16022
16183
|
};
|
|
16023
16184
|
var isReviewStep = (k) => STEP_DEF[k].sink === "review";
|
|
16024
16185
|
var REVIEW_STEP_KINDS = Object.keys(STEP_DEF).filter(isReviewStep);
|
|
@@ -16095,6 +16256,33 @@ var UpdateTodoRequestSchema = external_exports.object({
|
|
|
16095
16256
|
});
|
|
16096
16257
|
var MachineCreateTodoRequestSchema = CreateTodoRequestSchema.extend({ buildId: external_exports.string() });
|
|
16097
16258
|
var MachineUpdateTodoRequestSchema = UpdateTodoRequestSchema.omit({ assignment: true }).extend({ buildId: external_exports.string() });
|
|
16259
|
+
var UpdateChiefRequestSchema = external_exports.object({
|
|
16260
|
+
agent: AgentEntrySchema.nullable().optional(),
|
|
16261
|
+
charter: external_exports.string().optional()
|
|
16262
|
+
});
|
|
16263
|
+
var ChiefStepRequestSchema = external_exports.object({
|
|
16264
|
+
content: external_exports.string().trim().min(1)
|
|
16265
|
+
});
|
|
16266
|
+
var DispatchItemSchema = external_exports.object({
|
|
16267
|
+
todoId: external_exports.string().min(1),
|
|
16268
|
+
withPlan: external_exports.boolean().optional(),
|
|
16269
|
+
agent: AgentEntrySchema.optional(),
|
|
16270
|
+
reason: external_exports.string().optional()
|
|
16271
|
+
});
|
|
16272
|
+
var MAX_DISPATCH_ITEMS = 20;
|
|
16273
|
+
var MachineProposeBuildsRequestSchema = external_exports.object({
|
|
16274
|
+
chiefId: external_exports.string().min(1),
|
|
16275
|
+
items: external_exports.array(DispatchItemSchema).min(1).max(MAX_DISPATCH_ITEMS),
|
|
16276
|
+
summary: external_exports.string().optional()
|
|
16277
|
+
});
|
|
16278
|
+
var MachineRunBuildsRequestSchema = external_exports.object({
|
|
16279
|
+
chiefId: external_exports.string().min(1),
|
|
16280
|
+
items: external_exports.array(DispatchItemSchema).min(1).max(MAX_DISPATCH_ITEMS)
|
|
16281
|
+
});
|
|
16282
|
+
var MachineUpdateMemoryRequestSchema = external_exports.object({
|
|
16283
|
+
chiefId: external_exports.string().min(1),
|
|
16284
|
+
content: external_exports.string().min(1)
|
|
16285
|
+
});
|
|
16098
16286
|
var TriggerBuildRequestSchema = external_exports.object({
|
|
16099
16287
|
todoIds: external_exports.array(external_exports.string()).min(1),
|
|
16100
16288
|
// One-time assignment override; falls back to todo.assignment. Server enriches model.
|
|
@@ -16183,6 +16371,7 @@ var PollClient = class {
|
|
|
16183
16371
|
runningTasks = /* @__PURE__ */ new Map();
|
|
16184
16372
|
presenceTimer = null;
|
|
16185
16373
|
gcTimer = null;
|
|
16374
|
+
pokeTimer = null;
|
|
16186
16375
|
lastSentTunnelKey;
|
|
16187
16376
|
revoked = false;
|
|
16188
16377
|
maxConcurrentTasks = null;
|
|
@@ -16223,6 +16412,16 @@ var PollClient = class {
|
|
|
16223
16412
|
tunnel?.setNodeChangeHandler(() => void this.reportPresence());
|
|
16224
16413
|
void this.reportPresence();
|
|
16225
16414
|
}
|
|
16415
|
+
// Out-of-cycle beat for mirror-health transitions, so the web flips within ~a second
|
|
16416
|
+
// instead of waiting out the ≤20s presence tail. Debounced: a burst of transitions (daemon
|
|
16417
|
+
// restart syncing every mount at once) collapses into one POST carrying the final state.
|
|
16418
|
+
pokePresence() {
|
|
16419
|
+
if (this.pokeTimer) return;
|
|
16420
|
+
this.pokeTimer = setTimeout(() => {
|
|
16421
|
+
this.pokeTimer = null;
|
|
16422
|
+
void this.reportPresence();
|
|
16423
|
+
}, 250);
|
|
16424
|
+
}
|
|
16226
16425
|
maybeStartClaim() {
|
|
16227
16426
|
if (this.running && this.canClaim && !this.loopActive) {
|
|
16228
16427
|
this.loopActive = true;
|
|
@@ -16257,6 +16456,10 @@ var PollClient = class {
|
|
|
16257
16456
|
clearInterval(this.gcTimer);
|
|
16258
16457
|
this.gcTimer = null;
|
|
16259
16458
|
}
|
|
16459
|
+
if (this.pokeTimer) {
|
|
16460
|
+
clearTimeout(this.pokeTimer);
|
|
16461
|
+
this.pokeTimer = null;
|
|
16462
|
+
}
|
|
16260
16463
|
}
|
|
16261
16464
|
// The machine was removed from its team (or its token revoked): every /api/machine/* call now
|
|
16262
16465
|
// 401/403s. Fail fast instead of spinning forever — stop and exit so the operator notices.
|
|
@@ -16413,10 +16616,12 @@ function sleep(ms, signal) {
|
|
|
16413
16616
|
}
|
|
16414
16617
|
|
|
16415
16618
|
// src/SyncManager.ts
|
|
16416
|
-
var import_node_child_process2 = require("node:child_process");
|
|
16417
16619
|
var import_node_os4 = require("node:os");
|
|
16418
16620
|
var import_node_path6 = require("node:path");
|
|
16419
16621
|
|
|
16622
|
+
// src/lib/sidecar.ts
|
|
16623
|
+
var import_node_child_process2 = require("node:child_process");
|
|
16624
|
+
|
|
16420
16625
|
// src/lib/bin.ts
|
|
16421
16626
|
var import_node_fs5 = require("node:fs");
|
|
16422
16627
|
var import_node_path5 = require("node:path");
|
|
@@ -16482,31 +16687,24 @@ function createNdjsonParser(onEvent) {
|
|
|
16482
16687
|
};
|
|
16483
16688
|
}
|
|
16484
16689
|
|
|
16485
|
-
// src/
|
|
16690
|
+
// src/lib/sidecar.ts
|
|
16486
16691
|
var RESTART_MIN_MS = 1e3;
|
|
16487
16692
|
var RESTART_MAX_MS = 6e4;
|
|
16488
|
-
|
|
16489
|
-
|
|
16490
|
-
|
|
16491
|
-
|
|
16492
|
-
|
|
16493
|
-
|
|
16494
|
-
// by mountId
|
|
16495
|
-
constructor(serverUrl, token, machineId) {
|
|
16496
|
-
this.serverUrl = serverUrl;
|
|
16497
|
-
this.token = token;
|
|
16498
|
-
this.machineId = machineId;
|
|
16693
|
+
var Sidecar = class {
|
|
16694
|
+
constructor(args2, label, onEvent, onExit) {
|
|
16695
|
+
this.args = args2;
|
|
16696
|
+
this.label = label;
|
|
16697
|
+
this.onExit = onExit;
|
|
16698
|
+
this.parse = createNdjsonParser(onEvent);
|
|
16499
16699
|
}
|
|
16500
16700
|
proc = null;
|
|
16501
16701
|
stopped = false;
|
|
16502
16702
|
restartTimer = null;
|
|
16503
16703
|
restartDelayMs = RESTART_MIN_MS;
|
|
16504
|
-
|
|
16505
|
-
health = /* @__PURE__ */ new Map();
|
|
16704
|
+
parse;
|
|
16506
16705
|
start() {
|
|
16507
16706
|
this.stopped = false;
|
|
16508
|
-
|
|
16509
|
-
this.spawnSidecar();
|
|
16707
|
+
this.spawn();
|
|
16510
16708
|
}
|
|
16511
16709
|
stop() {
|
|
16512
16710
|
this.stopped = true;
|
|
@@ -16517,6 +16715,88 @@ var SyncManager = class {
|
|
|
16517
16715
|
this.proc?.kill();
|
|
16518
16716
|
this.proc = null;
|
|
16519
16717
|
}
|
|
16718
|
+
// Owner saw the sidecar's healthy signal — future crashes retry quickly again.
|
|
16719
|
+
resetBackoff() {
|
|
16720
|
+
this.restartDelayMs = RESTART_MIN_MS;
|
|
16721
|
+
}
|
|
16722
|
+
// Whether control messages can be sent at all (process up with a writable stdin).
|
|
16723
|
+
alive() {
|
|
16724
|
+
return Boolean(this.proc && !this.proc.killed && this.proc.stdin && !this.proc.stdin.destroyed);
|
|
16725
|
+
}
|
|
16726
|
+
// Write one NDJSON control message to the sidecar's stdin; false when it can't be delivered
|
|
16727
|
+
// (the exit/respawn path is the recovery, so callers just skip).
|
|
16728
|
+
write(msg) {
|
|
16729
|
+
const stdin = this.proc?.stdin;
|
|
16730
|
+
if (!stdin || stdin.destroyed) return false;
|
|
16731
|
+
try {
|
|
16732
|
+
stdin.write(JSON.stringify(msg) + "\n");
|
|
16733
|
+
return true;
|
|
16734
|
+
} catch {
|
|
16735
|
+
return false;
|
|
16736
|
+
}
|
|
16737
|
+
}
|
|
16738
|
+
spawn() {
|
|
16739
|
+
try {
|
|
16740
|
+
this.proc = (0, import_node_child_process2.spawn)(TUNNEL_BIN, this.args, { stdio: ["pipe", "pipe", "inherit"] });
|
|
16741
|
+
} catch {
|
|
16742
|
+
console.warn(`[${this.label}] tds-tunnel not available \u2014 disabled`);
|
|
16743
|
+
return;
|
|
16744
|
+
}
|
|
16745
|
+
this.proc.on("error", () => {
|
|
16746
|
+
console.warn(`[${this.label}] sidecar failed to start \u2014 disabled`);
|
|
16747
|
+
this.proc = null;
|
|
16748
|
+
});
|
|
16749
|
+
this.proc.on("exit", () => {
|
|
16750
|
+
this.proc = null;
|
|
16751
|
+
this.onExit?.();
|
|
16752
|
+
this.scheduleRestart();
|
|
16753
|
+
});
|
|
16754
|
+
this.proc.stdout?.on("data", (d) => this.parse(String(d)));
|
|
16755
|
+
this.proc.stdin?.on("error", () => {
|
|
16756
|
+
});
|
|
16757
|
+
}
|
|
16758
|
+
scheduleRestart() {
|
|
16759
|
+
if (this.stopped || this.restartTimer) return;
|
|
16760
|
+
const delay2 = this.restartDelayMs;
|
|
16761
|
+
this.restartDelayMs = Math.min(this.restartDelayMs * 2, RESTART_MAX_MS);
|
|
16762
|
+
console.warn(`[${this.label}] sidecar exited \u2014 restarting in ${Math.round(delay2 / 1e3)}s`);
|
|
16763
|
+
this.restartTimer = setTimeout(() => {
|
|
16764
|
+
this.restartTimer = null;
|
|
16765
|
+
this.spawn();
|
|
16766
|
+
}, delay2);
|
|
16767
|
+
}
|
|
16768
|
+
};
|
|
16769
|
+
|
|
16770
|
+
// src/SyncManager.ts
|
|
16771
|
+
function expandHome(p) {
|
|
16772
|
+
if (p === "~") return (0, import_node_os4.homedir)();
|
|
16773
|
+
if (p.startsWith("~/") || p.startsWith("~\\")) return (0, import_node_path6.join)((0, import_node_os4.homedir)(), p.slice(2));
|
|
16774
|
+
return p;
|
|
16775
|
+
}
|
|
16776
|
+
var WORKTREE_REBUILD_PATIENCE_MS = 18e4;
|
|
16777
|
+
var SyncManager = class {
|
|
16778
|
+
constructor(serverUrl, token, machineId) {
|
|
16779
|
+
this.machineId = machineId;
|
|
16780
|
+
this.sidecar = new Sidecar(
|
|
16781
|
+
["mirror", "--server", serverUrl, "--token", token, "--machine", machineId],
|
|
16782
|
+
"runner",
|
|
16783
|
+
(e) => this.onEvent(e)
|
|
16784
|
+
);
|
|
16785
|
+
}
|
|
16786
|
+
sidecar;
|
|
16787
|
+
desired = [];
|
|
16788
|
+
health = /* @__PURE__ */ new Map();
|
|
16789
|
+
// by mountId
|
|
16790
|
+
// Fired on coarse health transitions (first cycle, error set/cleared) so the presence beat
|
|
16791
|
+
// carrying the change goes out immediately instead of on the next 20s tick.
|
|
16792
|
+
onHealthChanged;
|
|
16793
|
+
start() {
|
|
16794
|
+
console.log(`[machine] runner: managing worktree mirrors (machineId=${this.machineId})`);
|
|
16795
|
+
this.sidecar.start();
|
|
16796
|
+
}
|
|
16797
|
+
stop() {
|
|
16798
|
+
this.sidecar.stop();
|
|
16799
|
+
}
|
|
16520
16800
|
// Desired mounts, straight off the presence response. Declarative: resolve dests, remember the
|
|
16521
16801
|
// set (a respawned daemon gets it replayed on its 'up' event), and forward it.
|
|
16522
16802
|
setMounts(resolved) {
|
|
@@ -16545,64 +16825,36 @@ var SyncManager = class {
|
|
|
16545
16825
|
};
|
|
16546
16826
|
});
|
|
16547
16827
|
}
|
|
16548
|
-
|
|
16549
|
-
try {
|
|
16550
|
-
this.proc = (0, import_node_child_process2.spawn)(
|
|
16551
|
-
TUNNEL_BIN,
|
|
16552
|
-
["mirror", "--server", this.serverUrl, "--token", this.token, "--machine", this.machineId],
|
|
16553
|
-
// stdin is piped: the desired mount set goes down as NDJSON control messages.
|
|
16554
|
-
{ stdio: ["pipe", "pipe", "inherit"] }
|
|
16555
|
-
);
|
|
16556
|
-
} catch {
|
|
16557
|
-
console.warn("[machine] runner: tds-tunnel not available \u2014 mirroring disabled");
|
|
16558
|
-
return;
|
|
16559
|
-
}
|
|
16560
|
-
this.proc.on("error", () => {
|
|
16561
|
-
console.warn("[machine] runner: mirror daemon failed to start \u2014 mirroring disabled");
|
|
16562
|
-
this.proc = null;
|
|
16563
|
-
});
|
|
16564
|
-
this.proc.on("exit", () => {
|
|
16565
|
-
this.proc = null;
|
|
16566
|
-
this.scheduleRestart();
|
|
16567
|
-
});
|
|
16568
|
-
this.proc.stdout?.on("data", (d) => this.onStdout(String(d)));
|
|
16569
|
-
this.proc.stdin?.on("error", () => {
|
|
16570
|
-
});
|
|
16571
|
-
}
|
|
16572
|
-
scheduleRestart() {
|
|
16573
|
-
if (this.stopped || this.restartTimer) return;
|
|
16574
|
-
const delay2 = this.restartDelayMs;
|
|
16575
|
-
this.restartDelayMs = Math.min(this.restartDelayMs * 2, RESTART_MAX_MS);
|
|
16576
|
-
console.warn(`[machine] runner: mirror daemon exited \u2014 restarting in ${Math.round(delay2 / 1e3)}s`);
|
|
16577
|
-
this.restartTimer = setTimeout(() => {
|
|
16578
|
-
this.restartTimer = null;
|
|
16579
|
-
this.spawnSidecar();
|
|
16580
|
-
}, delay2);
|
|
16581
|
-
}
|
|
16582
|
-
onStdout = createNdjsonParser((e) => {
|
|
16828
|
+
onEvent(e) {
|
|
16583
16829
|
if (e.event === "up") {
|
|
16584
|
-
this.
|
|
16830
|
+
this.sidecar.resetBackoff();
|
|
16585
16831
|
this.sendMounts();
|
|
16586
16832
|
} else if (e.event === "synced" && e.mount) {
|
|
16833
|
+
const prev = this.health.get(e.mount);
|
|
16587
16834
|
this.health.set(e.mount, { syncedAt: Date.now(), lastError: null });
|
|
16835
|
+
if (!prev?.syncedAt || prev.lastError) this.onHealthChanged?.();
|
|
16588
16836
|
if (e.changed || e.deleted) console.log(`[machine] runner: mount ${e.mount} \u2193 ${e.changed ?? 0} changed, ${e.deleted ?? 0} deleted`);
|
|
16837
|
+
} else if (e.event === "worktree-missing" && e.mount) {
|
|
16838
|
+
const prev = this.health.get(e.mount);
|
|
16839
|
+
const missingSince = prev?.missingSince ?? Date.now();
|
|
16840
|
+
const lastError = Date.now() - missingSince > WORKTREE_REBUILD_PATIENCE_MS ? e.message ?? "worktree missing" : null;
|
|
16841
|
+
this.health.set(e.mount, { syncedAt: 0, lastError, missingSince });
|
|
16842
|
+
if (!!prev?.syncedAt || (prev?.lastError ?? null) !== lastError) this.onHealthChanged?.();
|
|
16843
|
+
console.log(`[machine] runner: mount ${e.mount}: worktree missing \u2014 rebuild requested`);
|
|
16589
16844
|
} else if (e.event === "error") {
|
|
16590
16845
|
if (e.mount) {
|
|
16591
|
-
const
|
|
16592
|
-
|
|
16593
|
-
|
|
16846
|
+
const prev = this.health.get(e.mount);
|
|
16847
|
+
const message = e.message ?? "error";
|
|
16848
|
+
this.health.set(e.mount, { syncedAt: prev?.syncedAt ?? 0, lastError: message });
|
|
16849
|
+
if (prev?.lastError !== message) this.onHealthChanged?.();
|
|
16850
|
+
console.warn(`[machine] runner: mount ${e.mount}: ${message}`);
|
|
16594
16851
|
} else {
|
|
16595
16852
|
console.warn(`[machine] runner: ${e.message ?? "error"}`);
|
|
16596
16853
|
}
|
|
16597
16854
|
}
|
|
16598
|
-
}
|
|
16855
|
+
}
|
|
16599
16856
|
sendMounts() {
|
|
16600
|
-
|
|
16601
|
-
if (!stdin || stdin.destroyed) return;
|
|
16602
|
-
try {
|
|
16603
|
-
stdin.write(JSON.stringify({ cmd: "mounts", mounts: this.desired }) + "\n");
|
|
16604
|
-
} catch {
|
|
16605
|
-
}
|
|
16857
|
+
this.sidecar.write({ cmd: "mounts", mounts: this.desired });
|
|
16606
16858
|
}
|
|
16607
16859
|
};
|
|
16608
16860
|
|
|
@@ -16714,26 +16966,30 @@ async function syncModelsToServer(serverUrl, token, runtime) {
|
|
|
16714
16966
|
}
|
|
16715
16967
|
|
|
16716
16968
|
// src/lib/tunnel.ts
|
|
16717
|
-
var import_node_child_process3 = require("node:child_process");
|
|
16718
16969
|
var SHELL_BUFFER_CAP = 256 * 1024;
|
|
16719
|
-
var RESTART_MIN_MS2 = 1e3;
|
|
16720
|
-
var RESTART_MAX_MS2 = 6e4;
|
|
16721
16970
|
var EMPTY_BUFFER = Buffer.alloc(0);
|
|
16722
16971
|
var TunnelServer = class _TunnelServer {
|
|
16723
|
-
constructor(serverUrl, token) {
|
|
16724
|
-
this.serverUrl = serverUrl;
|
|
16725
|
-
this.token = token;
|
|
16726
|
-
}
|
|
16727
|
-
proc = null;
|
|
16728
16972
|
node = null;
|
|
16729
16973
|
onChange = null;
|
|
16730
|
-
|
|
16731
|
-
|
|
16732
|
-
|
|
16974
|
+
// Process lifecycle (spawn / respawn-with-backoff / stdin control) is the shared Sidecar's job;
|
|
16975
|
+
// this class keeps only the domain state that dies with the process: node + shells.
|
|
16976
|
+
sidecar;
|
|
16733
16977
|
// Reverse shells, keyed by (build, machine) — a build mirrored onto several runners has one shell
|
|
16734
16978
|
// per machine. Output is buffered even with no live subscriber so output a backgrounded command
|
|
16735
16979
|
// emits between tool calls survives until the next drain.
|
|
16736
16980
|
shells = /* @__PURE__ */ new Map();
|
|
16981
|
+
constructor(serverUrl, token) {
|
|
16982
|
+
this.sidecar = new Sidecar(
|
|
16983
|
+
["serve", "--server", serverUrl, "--token", token],
|
|
16984
|
+
"tunnel",
|
|
16985
|
+
(evt) => this.onEvent(evt),
|
|
16986
|
+
() => {
|
|
16987
|
+
this.node = null;
|
|
16988
|
+
this.closeAllShells();
|
|
16989
|
+
this.onChange?.();
|
|
16990
|
+
}
|
|
16991
|
+
);
|
|
16992
|
+
}
|
|
16737
16993
|
// Set the single handler fired whenever the tunnel node appears/changes/clears, so presence can
|
|
16738
16994
|
// be pushed immediately instead of waiting for the next heartbeat tick. Fires now if already ready.
|
|
16739
16995
|
setNodeChangeHandler(cb) {
|
|
@@ -16741,56 +16997,16 @@ var TunnelServer = class _TunnelServer {
|
|
|
16741
16997
|
if (this.node) cb();
|
|
16742
16998
|
}
|
|
16743
16999
|
start() {
|
|
16744
|
-
this.
|
|
16745
|
-
this.spawnSidecar();
|
|
16746
|
-
}
|
|
16747
|
-
spawnSidecar() {
|
|
16748
|
-
try {
|
|
16749
|
-
this.proc = (0, import_node_child_process3.spawn)(
|
|
16750
|
-
TUNNEL_BIN,
|
|
16751
|
-
["serve", "--server", this.serverUrl, "--token", this.token],
|
|
16752
|
-
// stdin is piped (not ignored) so we can write reverse-shell control messages to the sidecar.
|
|
16753
|
-
{ stdio: ["pipe", "pipe", "inherit"] }
|
|
16754
|
-
);
|
|
16755
|
-
} catch {
|
|
16756
|
-
console.warn("[tunnel] sidecar not available \u2014 runner mirroring disabled");
|
|
16757
|
-
return;
|
|
16758
|
-
}
|
|
16759
|
-
this.proc.on("error", () => {
|
|
16760
|
-
console.warn("[tunnel] sidecar failed to start \u2014 runner mirroring disabled");
|
|
16761
|
-
this.proc = null;
|
|
16762
|
-
});
|
|
16763
|
-
this.proc.on("exit", () => {
|
|
16764
|
-
this.proc = null;
|
|
16765
|
-
this.node = null;
|
|
16766
|
-
this.closeAllShells();
|
|
16767
|
-
this.onChange?.();
|
|
16768
|
-
this.scheduleRestart();
|
|
16769
|
-
});
|
|
16770
|
-
this.proc.stdout?.on("data", (d) => this.onStdout(String(d)));
|
|
16771
|
-
this.proc.stdin?.on("error", () => {
|
|
16772
|
-
});
|
|
16773
|
-
}
|
|
16774
|
-
scheduleRestart() {
|
|
16775
|
-
if (this.stopped || this.restartTimer) return;
|
|
16776
|
-
const delay2 = this.restartDelayMs;
|
|
16777
|
-
this.restartDelayMs = Math.min(this.restartDelayMs * 2, RESTART_MAX_MS2);
|
|
16778
|
-
console.warn(`[tunnel] sidecar exited \u2014 restarting in ${Math.round(delay2 / 1e3)}s`);
|
|
16779
|
-
this.restartTimer = setTimeout(() => {
|
|
16780
|
-
this.restartTimer = null;
|
|
16781
|
-
this.spawnSidecar();
|
|
16782
|
-
}, delay2);
|
|
17000
|
+
this.sidecar.start();
|
|
16783
17001
|
}
|
|
16784
17002
|
// Reverse-shell map key: a build mirrored onto several runners has one shell per machine.
|
|
16785
17003
|
static shellKey(build, machine) {
|
|
16786
17004
|
return `${build}\0${machine}`;
|
|
16787
17005
|
}
|
|
16788
|
-
|
|
16789
|
-
// shell-opened/shell-output/shell-closed carry reverse-shell traffic for a (build, machine).
|
|
16790
|
-
onStdout = createNdjsonParser((evt) => {
|
|
17006
|
+
onEvent(evt) {
|
|
16791
17007
|
if (evt.event === "ready" && evt.tailnetAddr && evt.sshHostKey) {
|
|
16792
17008
|
this.node = { tailnetAddr: evt.tailnetAddr, sshHostKey: evt.sshHostKey };
|
|
16793
|
-
this.
|
|
17009
|
+
this.sidecar.resetBackoff();
|
|
16794
17010
|
console.log(`[tunnel] ready (tailnet ${evt.tailnetAddr})`);
|
|
16795
17011
|
this.onChange?.();
|
|
16796
17012
|
} else if ((evt.event === "shell-opened" || evt.event === "shell-output" || evt.event === "shell-closed") && evt.build) {
|
|
@@ -16814,27 +17030,17 @@ var TunnelServer = class _TunnelServer {
|
|
|
16814
17030
|
} else if (evt.event === "error") {
|
|
16815
17031
|
console.warn(`[tunnel] ${evt.message ?? "error"}`);
|
|
16816
17032
|
}
|
|
16817
|
-
}
|
|
17033
|
+
}
|
|
16818
17034
|
// --- Reverse shell (agent → user's machine via the sync client) -----------------------------
|
|
16819
17035
|
// Whether reverse-shell control can be sent at all (sidecar up with a writable stdin). The tool
|
|
16820
17036
|
// checks this to fail fast instead of writing into the void.
|
|
16821
17037
|
shellAvailable() {
|
|
16822
|
-
return
|
|
16823
|
-
}
|
|
16824
|
-
writeControl(msg) {
|
|
16825
|
-
const stdin = this.proc?.stdin;
|
|
16826
|
-
if (!stdin || stdin.destroyed) return false;
|
|
16827
|
-
try {
|
|
16828
|
-
stdin.write(JSON.stringify(msg) + "\n");
|
|
16829
|
-
return true;
|
|
16830
|
-
} catch {
|
|
16831
|
-
return false;
|
|
16832
|
-
}
|
|
17038
|
+
return this.sidecar.alive();
|
|
16833
17039
|
}
|
|
16834
17040
|
// Send keystrokes/command text to a (build, machine) shell. data is UTF-8 text (may include control
|
|
16835
17041
|
// bytes like \x03 for Ctrl-C); it's base64-framed on the wire.
|
|
16836
17042
|
shellSendInput(build, machine, data) {
|
|
16837
|
-
return this.
|
|
17043
|
+
return this.sidecar.write({ cmd: "shell-input", build, machine, data: Buffer.from(data, "utf8").toString("base64") });
|
|
16838
17044
|
}
|
|
16839
17045
|
// Whether the client's shell channel is currently registered sidecar-side. undefined = no event
|
|
16840
17046
|
// seen yet for this key (a client may still be connected — input discovers it); false = it was
|
|
@@ -16891,13 +17097,7 @@ var TunnelServer = class _TunnelServer {
|
|
|
16891
17097
|
return this.node;
|
|
16892
17098
|
}
|
|
16893
17099
|
stop() {
|
|
16894
|
-
this.
|
|
16895
|
-
if (this.restartTimer) {
|
|
16896
|
-
clearTimeout(this.restartTimer);
|
|
16897
|
-
this.restartTimer = null;
|
|
16898
|
-
}
|
|
16899
|
-
this.proc?.kill();
|
|
16900
|
-
this.proc = null;
|
|
17100
|
+
this.sidecar.stop();
|
|
16901
17101
|
this.node = null;
|
|
16902
17102
|
this.closeAllShells();
|
|
16903
17103
|
}
|
|
@@ -16957,6 +17157,7 @@ async function startCommand(args2) {
|
|
|
16957
17157
|
const { flags, bools } = parseFlags(args2, { f: "foreground" });
|
|
16958
17158
|
if (bools.supervisor) return runSupervisor(args2);
|
|
16959
17159
|
if (bools.foreground) {
|
|
17160
|
+
installTimestampedLogging();
|
|
16960
17161
|
const { config: config2, serverUrl } = await ensureEnrolled(flags);
|
|
16961
17162
|
return runWorker(config2, serverUrl);
|
|
16962
17163
|
}
|
|
@@ -17089,6 +17290,7 @@ async function runWorker(config2, serverUrl) {
|
|
|
17089
17290
|
syncManager.start();
|
|
17090
17291
|
syncManager.setMounts(lastMounts);
|
|
17091
17292
|
client.getMountSync = () => syncManager?.status() ?? null;
|
|
17293
|
+
syncManager.onHealthChanged = () => client.pokePresence();
|
|
17092
17294
|
};
|
|
17093
17295
|
const disableRunner = () => {
|
|
17094
17296
|
if (!syncManager) return;
|
|
@@ -17788,6 +17990,32 @@ async function logoutCommand() {
|
|
|
17788
17990
|
console.log("[tds] Run 'tds start' to enroll again.");
|
|
17789
17991
|
}
|
|
17790
17992
|
|
|
17993
|
+
// src/lib/version.ts
|
|
17994
|
+
var import_fs6 = require("fs");
|
|
17995
|
+
var import_path2 = require("path");
|
|
17996
|
+
function getVersion() {
|
|
17997
|
+
let dir = __dirname;
|
|
17998
|
+
for (let i = 0; i < 6; i++) {
|
|
17999
|
+
const p = (0, import_path2.join)(dir, "package.json");
|
|
18000
|
+
if ((0, import_fs6.existsSync)(p)) {
|
|
18001
|
+
try {
|
|
18002
|
+
const pkg = JSON.parse((0, import_fs6.readFileSync)(p, "utf-8"));
|
|
18003
|
+
if (pkg.version) return pkg.version;
|
|
18004
|
+
} catch {
|
|
18005
|
+
}
|
|
18006
|
+
}
|
|
18007
|
+
const parent = (0, import_path2.dirname)(dir);
|
|
18008
|
+
if (parent === dir) break;
|
|
18009
|
+
dir = parent;
|
|
18010
|
+
}
|
|
18011
|
+
return "unknown";
|
|
18012
|
+
}
|
|
18013
|
+
|
|
18014
|
+
// src/commands/version.ts
|
|
18015
|
+
async function versionCommand() {
|
|
18016
|
+
console.log(`tds ${getVersion()}`);
|
|
18017
|
+
}
|
|
18018
|
+
|
|
17791
18019
|
// src/index.ts
|
|
17792
18020
|
var COMMANDS = {
|
|
17793
18021
|
start: {
|
|
@@ -17857,6 +18085,13 @@ Stop the background daemon (if running), then clear ~/.tds/machine.json so the n
|
|
|
17857
18085
|
help: `Usage: tds status
|
|
17858
18086
|
|
|
17859
18087
|
Show this machine's registration and current status.`
|
|
18088
|
+
},
|
|
18089
|
+
version: {
|
|
18090
|
+
run: () => versionCommand(),
|
|
18091
|
+
summary: "Show the tds CLI version",
|
|
18092
|
+
help: `Usage: tds version
|
|
18093
|
+
|
|
18094
|
+
Print the installed tds CLI version.`
|
|
17860
18095
|
},
|
|
17861
18096
|
provider: {
|
|
17862
18097
|
run: providerCommand,
|
|
@@ -17875,6 +18110,10 @@ Subcommands:
|
|
|
17875
18110
|
var args = process.argv.slice(2);
|
|
17876
18111
|
var cmd = args[0];
|
|
17877
18112
|
var rest = args.slice(1);
|
|
18113
|
+
if (cmd === "--version" || cmd === "-v") {
|
|
18114
|
+
console.log(`tds ${getVersion()}`);
|
|
18115
|
+
process.exit(0);
|
|
18116
|
+
}
|
|
17878
18117
|
if (!cmd || cmd === "--help" || cmd === "-h" || cmd === "help") {
|
|
17879
18118
|
const topic = cmd === "help" ? rest[0] : void 0;
|
|
17880
18119
|
if (topic && COMMANDS[topic]) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@todos-dev/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"bin": {
|
|
5
5
|
"tds": "dist/index.js"
|
|
6
6
|
},
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
"@tds/types": "0.1.0"
|
|
28
28
|
},
|
|
29
29
|
"optionalDependencies": {
|
|
30
|
-
"@todos-dev/cli-darwin-arm64": "0.1.
|
|
31
|
-
"@todos-dev/cli-darwin-x64": "0.1.
|
|
32
|
-
"@todos-dev/cli-linux-x64": "0.1.
|
|
33
|
-
"@todos-dev/cli-linux-arm64": "0.1.
|
|
34
|
-
"@todos-dev/cli-win32-x64": "0.1.
|
|
35
|
-
"@todos-dev/cli-win32-arm64": "0.1.
|
|
30
|
+
"@todos-dev/cli-darwin-arm64": "0.1.5",
|
|
31
|
+
"@todos-dev/cli-darwin-x64": "0.1.5",
|
|
32
|
+
"@todos-dev/cli-linux-x64": "0.1.5",
|
|
33
|
+
"@todos-dev/cli-linux-arm64": "0.1.5",
|
|
34
|
+
"@todos-dev/cli-win32-x64": "0.1.5",
|
|
35
|
+
"@todos-dev/cli-win32-arm64": "0.1.5"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"dev": "tsx watch src/index.ts",
|