dahrk-node 0.1.4 → 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/main.js +509 -43
- package/package.json +2 -2
package/dist/main.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/main.ts
|
|
4
|
-
import { existsSync as
|
|
4
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync7, readFileSync as readFileSync5, realpathSync as realpathSync3, writeFileSync as writeFileSync7 } from "fs";
|
|
5
5
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
6
|
-
import { homedir as
|
|
7
|
-
import { basename, join as
|
|
6
|
+
import { homedir as homedir4 } from "os";
|
|
7
|
+
import { basename, join as join9 } from "path";
|
|
8
8
|
import { pathToFileURL } from "url";
|
|
9
9
|
|
|
10
10
|
// ../../packages/edge/src/ws-client.ts
|
|
@@ -22,11 +22,11 @@ function createMockRunner(runtime) {
|
|
|
22
22
|
return {
|
|
23
23
|
runtime,
|
|
24
24
|
async runBatch(ctx, onTrace) {
|
|
25
|
-
const
|
|
25
|
+
const tool3 = ctx.config.tools?.[0] ?? "shell";
|
|
26
26
|
const toolUseId = "mock-tool-1";
|
|
27
27
|
const events = [
|
|
28
28
|
{ seq: 0, runtime, type: "thought", ts: nowIso(), text: "mock: planning the stage" },
|
|
29
|
-
{ seq: 1, runtime, type: "action", ts: nowIso(), tool:
|
|
29
|
+
{ seq: 1, runtime, type: "action", ts: nowIso(), tool: tool3, toolUseId, input: { note: "mock action" } },
|
|
30
30
|
{ seq: 2, runtime, type: "observation", ts: nowIso(), toolUseId, output: { ok: true } },
|
|
31
31
|
{ seq: 3, runtime, type: "response", ts: nowIso(), text: "mock: stage complete" }
|
|
32
32
|
];
|
|
@@ -371,6 +371,47 @@ function raceNextTurn(pending, idleMs, signal) {
|
|
|
371
371
|
);
|
|
372
372
|
});
|
|
373
373
|
}
|
|
374
|
+
function createElicitTurnRouter(turns, opts) {
|
|
375
|
+
const conversation = new ManagedMailbox();
|
|
376
|
+
const ref = { settle: null };
|
|
377
|
+
void (async () => {
|
|
378
|
+
try {
|
|
379
|
+
for await (const t of turns) {
|
|
380
|
+
const settle = ref.settle;
|
|
381
|
+
if (settle) settle({ kind: "reply", text: t.text });
|
|
382
|
+
else conversation.push(t);
|
|
383
|
+
}
|
|
384
|
+
} finally {
|
|
385
|
+
conversation.end();
|
|
386
|
+
const settle = ref.settle;
|
|
387
|
+
if (settle) settle({ kind: "cancel" });
|
|
388
|
+
}
|
|
389
|
+
})();
|
|
390
|
+
const ask = (firstReply, onRaise) => {
|
|
391
|
+
if (ref.settle) return Promise.resolve({ kind: "busy" });
|
|
392
|
+
onRaise();
|
|
393
|
+
return new Promise((resolve2) => {
|
|
394
|
+
let settled = false;
|
|
395
|
+
const finish = (o) => {
|
|
396
|
+
if (settled) return;
|
|
397
|
+
settled = true;
|
|
398
|
+
clearTimeout(timer);
|
|
399
|
+
opts.signal.removeEventListener("abort", onAbort);
|
|
400
|
+
ref.settle = null;
|
|
401
|
+
resolve2(o);
|
|
402
|
+
};
|
|
403
|
+
const onAbort = () => finish({ kind: "cancel" });
|
|
404
|
+
const timer = setTimeout(() => finish({ kind: "noreply" }), firstReply ? opts.firstReplyMs : opts.idleMs);
|
|
405
|
+
if (opts.signal.aborted) {
|
|
406
|
+
finish({ kind: "cancel" });
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
410
|
+
ref.settle = finish;
|
|
411
|
+
});
|
|
412
|
+
};
|
|
413
|
+
return { conversation, ask };
|
|
414
|
+
}
|
|
374
415
|
|
|
375
416
|
// ../../packages/executor-worktree/src/stage-complete-tool.ts
|
|
376
417
|
import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
|
|
@@ -401,6 +442,52 @@ function createStageCompleteTool() {
|
|
|
401
442
|
};
|
|
402
443
|
}
|
|
403
444
|
|
|
445
|
+
// ../../packages/executor-worktree/src/ask-user-question-tool.ts
|
|
446
|
+
import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
447
|
+
import { z as z2 } from "zod";
|
|
448
|
+
var ASK_USER_QUESTION_TOOL_NAME = "mcp__ask__ask_user_question";
|
|
449
|
+
var ASK_USER_QUESTION_ALIAS = "AskUserQuestion";
|
|
450
|
+
var optionSchema = z2.object({
|
|
451
|
+
label: z2.string(),
|
|
452
|
+
description: z2.string().optional().default(""),
|
|
453
|
+
preview: z2.string().optional()
|
|
454
|
+
});
|
|
455
|
+
var questionSchema = z2.object({
|
|
456
|
+
question: z2.string(),
|
|
457
|
+
header: z2.string().optional().default(""),
|
|
458
|
+
options: z2.array(optionSchema).min(1),
|
|
459
|
+
multiSelect: z2.boolean().optional()
|
|
460
|
+
});
|
|
461
|
+
function toElicitQuestion(q) {
|
|
462
|
+
const options = q.options.map((o) => ({ label: o.label, value: o.label }));
|
|
463
|
+
const described = q.options.filter((o) => (o.description ?? "").trim());
|
|
464
|
+
const lines = described.map((o) => `- ${o.label}: ${(o.description ?? "").trim()}`);
|
|
465
|
+
const prompt = lines.length ? `${q.question}
|
|
466
|
+
|
|
467
|
+
${lines.join("\n")}` : q.question;
|
|
468
|
+
return { prompt, options, ...q.multiSelect ? { multiSelect: true } : {} };
|
|
469
|
+
}
|
|
470
|
+
function createAskUserQuestionTool(deps) {
|
|
471
|
+
const askTool = tool2(
|
|
472
|
+
"ask_user_question",
|
|
473
|
+
"Ask the human a structured multiple-choice question and wait for their selection. Use this when you need the human to choose between options before you can continue.",
|
|
474
|
+
{ questions: z2.array(questionSchema).min(1) },
|
|
475
|
+
async (args) => {
|
|
476
|
+
const first = args.questions[0];
|
|
477
|
+
const question = toElicitQuestion(first);
|
|
478
|
+
const prompt = args.questions.length > 1 ? `${question.prompt}
|
|
479
|
+
|
|
480
|
+
(Note: ${args.questions.length} questions were asked at once; answer this one first, then ask the rest.)` : question.prompt;
|
|
481
|
+
const text = await deps.ask({ ...question, prompt });
|
|
482
|
+
return { content: [{ type: "text", text }] };
|
|
483
|
+
}
|
|
484
|
+
);
|
|
485
|
+
return {
|
|
486
|
+
server: createSdkMcpServer2({ name: "ask", version: "0.0.0", tools: [askTool] }),
|
|
487
|
+
allowedToolName: ASK_USER_QUESTION_TOOL_NAME
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
404
491
|
// ../../packages/executor-worktree/src/claude-adapter.ts
|
|
405
492
|
var COALESCE_MS = Number(process.env.DAHRK_COALESCE_MS ?? process.env.SKAKEL_COALESCE_MS ?? 40);
|
|
406
493
|
var MAX_TURNS = Number(process.env.DAHRK_MAX_TURNS ?? process.env.SKAKEL_MAX_TURNS ?? 64);
|
|
@@ -419,6 +506,10 @@ var policyCanUseTool = async (ctx, toolName, input) => {
|
|
|
419
506
|
}
|
|
420
507
|
return { behavior: "allow", updatedInput: input };
|
|
421
508
|
};
|
|
509
|
+
var interactiveCanUseTool = (summarising, stageAllowedToolName, ctx, toolName, input) => summarising && toolName !== stageAllowedToolName ? Promise.resolve({
|
|
510
|
+
behavior: "deny",
|
|
511
|
+
message: "Summarise from the work you just did; reply with the sentence only, no tools."
|
|
512
|
+
}) : policyCanUseTool(ctx, toolName, input);
|
|
422
513
|
function buildBrokeredMcpServers(ctx) {
|
|
423
514
|
const servers = ctx.config.mcpServers;
|
|
424
515
|
if (!servers || servers.length === 0 || !ctx.mcpProxyBaseUrl) return void 0;
|
|
@@ -507,6 +598,27 @@ function createClaudeRunner() {
|
|
|
507
598
|
async runInteractive(ctx, turns, onTrace) {
|
|
508
599
|
const emit = makeEmit("claude-code", onTrace);
|
|
509
600
|
const stageTool = createStageCompleteTool();
|
|
601
|
+
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
602
|
+
let awaitingFirstReply = true;
|
|
603
|
+
const router = createElicitTurnRouter(turns, { signal: abortController.signal, firstReplyMs, idleMs });
|
|
604
|
+
const askTool = createAskUserQuestionTool({
|
|
605
|
+
ask: async (question) => {
|
|
606
|
+
const outcome = await router.ask(awaitingFirstReply, () => {
|
|
607
|
+
emit({ type: "elicitation", prompt: question.prompt, signal: "select", options: question.options });
|
|
608
|
+
ctx.emitElicit?.(question);
|
|
609
|
+
});
|
|
610
|
+
switch (outcome.kind) {
|
|
611
|
+
case "reply":
|
|
612
|
+
return `The user selected: ${outcome.text}`;
|
|
613
|
+
case "busy":
|
|
614
|
+
return "Only one question can be asked at a time; wait for the current one to be answered, then ask again.";
|
|
615
|
+
case "noreply":
|
|
616
|
+
return "No response from the user; proceed with your best judgement.";
|
|
617
|
+
case "cancel":
|
|
618
|
+
return "The question was cancelled.";
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
});
|
|
510
622
|
const brokered = buildBrokeredMcpServers(ctx);
|
|
511
623
|
let summarising = false;
|
|
512
624
|
const options = {
|
|
@@ -514,12 +626,17 @@ function createClaudeRunner() {
|
|
|
514
626
|
// Keep the cwd-anchoring preset; fold the stage instruction in via `append` so the
|
|
515
627
|
// interactive persona still gets it without losing the working-directory context.
|
|
516
628
|
systemPrompt: hasSystemPrompt(ctx) ? { type: "preset", preset: "claude_code", append: resolveStagePrompt(ctx) } : CLAUDE_CODE_SYSTEM_PROMPT,
|
|
517
|
-
// Inject the stage-complete exit tool
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
//
|
|
521
|
-
|
|
522
|
-
|
|
629
|
+
// Inject the stage-complete exit tool and the AskUserQuestion shadow alongside any brokered
|
|
630
|
+
// MCP servers (parity with batch).
|
|
631
|
+
mcpServers: { dahrk: stageTool.server, ask: askTool.server, ...brokered ?? {} },
|
|
632
|
+
// Redirect the built-in AskUserQuestion to the shadow tool so a structured question surfaces
|
|
633
|
+
// as a Linear elicitation. The redirect is name-only and single-hop; the tool still runs, so
|
|
634
|
+
// this is mapping, not gating (DHK-344 / DHK-223).
|
|
635
|
+
toolAliases: { [ASK_USER_QUESTION_ALIAS]: askTool.allowedToolName },
|
|
636
|
+
// Auto-approve the injected tools; `allowedTools` is an auto-approve list, not a whitelist, so
|
|
637
|
+
// it does not restrict the other tools canUseTool allows.
|
|
638
|
+
allowedTools: [stageTool.allowedToolName, askTool.allowedToolName],
|
|
639
|
+
canUseTool: async (toolName, input) => interactiveCanUseTool(summarising, stageTool.allowedToolName, ctx, toolName, input),
|
|
523
640
|
maxTurns: MAX_TURNS,
|
|
524
641
|
includePartialMessages: false
|
|
525
642
|
};
|
|
@@ -529,7 +646,7 @@ function createClaudeRunner() {
|
|
|
529
646
|
const q = query({ prompt: mailbox, options });
|
|
530
647
|
active = q;
|
|
531
648
|
const it = q[Symbol.asyncIterator]();
|
|
532
|
-
const humanIter =
|
|
649
|
+
const humanIter = router.conversation[Symbol.asyncIterator]();
|
|
533
650
|
const state = newBufferState();
|
|
534
651
|
const consumeTurn = async () => {
|
|
535
652
|
for (; ; ) {
|
|
@@ -539,8 +656,6 @@ function createClaudeRunner() {
|
|
|
539
656
|
if (res.isResult) return res.responseText;
|
|
540
657
|
}
|
|
541
658
|
};
|
|
542
|
-
const { firstReplyMs, idleMs } = interactiveIdleWindows(ctx);
|
|
543
|
-
let awaitingFirstReply = true;
|
|
544
659
|
let exited = null;
|
|
545
660
|
let pending = humanIter.next();
|
|
546
661
|
try {
|
|
@@ -1230,8 +1345,11 @@ function sanitizeBranchName(name) {
|
|
|
1230
1345
|
if (!name) return name;
|
|
1231
1346
|
return name.replace(/[`~^:?*[\]\\@{}\s]/g, "-").replace(/\.{2,}/g, ".").replace(/\/{2,}/g, "/").replace(/\.lock(\/|$)/g, "$1").replace(/^[.\-/]+/, "").replace(/[.\-/]+$/, "").replace(/-{2,}/g, "-");
|
|
1232
1347
|
}
|
|
1348
|
+
function resolveWorktreesDir(override) {
|
|
1349
|
+
return override ?? process.env.DAHRK_WORKTREES_DIR ?? process.env.SKAKEL_WORKTREES_DIR ?? join2(homedir(), ".dahrk", "worktrees");
|
|
1350
|
+
}
|
|
1233
1351
|
function createGitService(opts = {}) {
|
|
1234
|
-
const worktreesDir = opts.worktreesDir
|
|
1352
|
+
const worktreesDir = resolveWorktreesDir(opts.worktreesDir);
|
|
1235
1353
|
const mirrorsDir = opts.mirrorsDir ?? process.env.DAHRK_MIRRORS_DIR ?? process.env.SKAKEL_MIRRORS_DIR ?? join2(homedir(), ".dahrk", "mirrors");
|
|
1236
1354
|
const authorName = opts.authorName ?? process.env.DAHRK_GIT_AUTHOR_NAME ?? "Dahrk";
|
|
1237
1355
|
const authorEmail = opts.authorEmail ?? process.env.DAHRK_GIT_AUTHOR_EMAIL ?? "noreply@dahrk.ai";
|
|
@@ -1334,6 +1452,7 @@ function createGitService(opts = {}) {
|
|
|
1334
1452
|
scratchPath: join2(worktreePath, ".skakel", "scratch")
|
|
1335
1453
|
});
|
|
1336
1454
|
return {
|
|
1455
|
+
worktreesDir,
|
|
1337
1456
|
async createWorktree(spec) {
|
|
1338
1457
|
const { repoId, gitUrl, baseBranch, runId } = spec;
|
|
1339
1458
|
const branchName = sanitizeBranchName(spec.branch ?? `dahrk/${runId}`);
|
|
@@ -1662,12 +1781,12 @@ function evaluatePolicies(event, rules) {
|
|
|
1662
1781
|
}
|
|
1663
1782
|
return ALLOW;
|
|
1664
1783
|
}
|
|
1665
|
-
function denyToolRule(
|
|
1784
|
+
function denyToolRule(tool3) {
|
|
1666
1785
|
return {
|
|
1667
1786
|
name: "deny_tool",
|
|
1668
1787
|
evaluate(event) {
|
|
1669
|
-
if (event.kind === "action" && event.tool ===
|
|
1670
|
-
return { verdict: "deny", policy: "deny_tool", reason: `tool "${
|
|
1788
|
+
if (event.kind === "action" && event.tool === tool3) {
|
|
1789
|
+
return { verdict: "deny", policy: "deny_tool", reason: `tool "${tool3}" is denied` };
|
|
1671
1790
|
}
|
|
1672
1791
|
return null;
|
|
1673
1792
|
}
|
|
@@ -1748,7 +1867,15 @@ function buildRules(policies, ctx) {
|
|
|
1748
1867
|
const rules = [];
|
|
1749
1868
|
let stageToolCalls = 0;
|
|
1750
1869
|
for (const p of policies) {
|
|
1751
|
-
if ("
|
|
1870
|
+
if ("read_only" in p && p.read_only) {
|
|
1871
|
+
rules.push({
|
|
1872
|
+
name: "read_only",
|
|
1873
|
+
evaluate(event) {
|
|
1874
|
+
if (event.kind !== "action" || !WRITE_TOOLS.has(event.tool)) return null;
|
|
1875
|
+
return { verdict: "deny", policy: "read_only", reason: `read-only stage: "${event.tool}" is not permitted` };
|
|
1876
|
+
}
|
|
1877
|
+
});
|
|
1878
|
+
} else if ("write_scope" in p) {
|
|
1752
1879
|
const { branches, repos } = p.write_scope;
|
|
1753
1880
|
rules.push({
|
|
1754
1881
|
name: "write_scope",
|
|
@@ -2249,11 +2376,11 @@ function createStageRunner(deps) {
|
|
|
2249
2376
|
let denied = false;
|
|
2250
2377
|
const authorisedActions = [];
|
|
2251
2378
|
const runtime = agentConfig.runtime;
|
|
2252
|
-
const actionKey = (
|
|
2379
|
+
const actionKey = (tool3, input) => {
|
|
2253
2380
|
try {
|
|
2254
|
-
return `${
|
|
2381
|
+
return `${tool3}\0${JSON.stringify(input)}`;
|
|
2255
2382
|
} catch {
|
|
2256
|
-
return `${
|
|
2383
|
+
return `${tool3}\0`;
|
|
2257
2384
|
}
|
|
2258
2385
|
};
|
|
2259
2386
|
const policyReason = (verdict) => verdict.reason ?? `tool action denied by ${verdict.policy}`;
|
|
@@ -2266,12 +2393,12 @@ function createStageRunner(deps) {
|
|
|
2266
2393
|
streamEvent(writer.append({ seq: 0, ts: nowIso2(), type: "state", runtime, event: "policy-deny", detail: reason }));
|
|
2267
2394
|
deps.sendProgress({ jobId, kind: "error", ts: nowIso2(), text: reason });
|
|
2268
2395
|
};
|
|
2269
|
-
const authorizeToolUse = (
|
|
2270
|
-
const verdict = evaluatePolicies({ kind: "action", stageId, tool:
|
|
2396
|
+
const authorizeToolUse = (tool3, input) => {
|
|
2397
|
+
const verdict = evaluatePolicies({ kind: "action", stageId, tool: tool3, input }, rules);
|
|
2271
2398
|
if (verdict.verdict === "deny") {
|
|
2272
2399
|
recordDeny(verdict);
|
|
2273
2400
|
} else {
|
|
2274
|
-
authorisedActions.push(actionKey(
|
|
2401
|
+
authorisedActions.push(actionKey(tool3, input));
|
|
2275
2402
|
}
|
|
2276
2403
|
return verdict;
|
|
2277
2404
|
};
|
|
@@ -2319,7 +2446,17 @@ function createStageRunner(deps) {
|
|
|
2319
2446
|
// The adapter persists each runtime-native record under the attempt's raw/ sidecar
|
|
2320
2447
|
// and stamps the rawRef onto the emitted event.
|
|
2321
2448
|
writeRaw: writer.writeRaw,
|
|
2322
|
-
authorizeToolUse
|
|
2449
|
+
authorizeToolUse,
|
|
2450
|
+
// Wire the interactive AskUserQuestion elicitation seam (DHK-344): the adapter calls this when
|
|
2451
|
+
// the agent asks a structured question, and the edge relays it to the hub as an `elicit` frame.
|
|
2452
|
+
...deps.sendElicit ? {
|
|
2453
|
+
emitElicit: (question) => deps.sendElicit({
|
|
2454
|
+
jobId,
|
|
2455
|
+
prompt: question.prompt,
|
|
2456
|
+
options: question.options,
|
|
2457
|
+
...question.multiSelect ? { multiSelect: true } : {}
|
|
2458
|
+
})
|
|
2459
|
+
} : {}
|
|
2323
2460
|
};
|
|
2324
2461
|
const interactive = agentConfig.interaction === "interactive";
|
|
2325
2462
|
let result;
|
|
@@ -2565,6 +2702,9 @@ async function startEdgeNode(opts) {
|
|
|
2565
2702
|
...opts.tenantId ? { tenantId: opts.tenantId } : {},
|
|
2566
2703
|
rules,
|
|
2567
2704
|
sendProgress: (progress) => send({ type: "progress", progress }),
|
|
2705
|
+
// DHK-344: relay a mid-interactive-stage AskUserQuestion to the hub as an `elicit` frame; the hub
|
|
2706
|
+
// raises the Linear elicitation and the human's pick returns on the existing `turn` frame.
|
|
2707
|
+
sendElicit: (frame) => send({ type: "elicit", ...frame }),
|
|
2568
2708
|
trace,
|
|
2569
2709
|
...opts.retention ? { retention: opts.retention } : {}
|
|
2570
2710
|
};
|
|
@@ -2684,7 +2824,11 @@ async function startEdgeNode(opts) {
|
|
|
2684
2824
|
...opts.name ? { name: opts.name } : {},
|
|
2685
2825
|
os: osPlatform(),
|
|
2686
2826
|
arch: osArch(),
|
|
2687
|
-
clientVersion: opts.clientVersion ?? "0.0.0"
|
|
2827
|
+
clientVersion: opts.clientVersion ?? "0.0.0",
|
|
2828
|
+
// Advertise the resolved worktree base so the hub records each run's real worktree location in
|
|
2829
|
+
// the projection instead of an advisory placeholder. Single-sourced from the git service so it
|
|
2830
|
+
// always matches where worktrees actually land.
|
|
2831
|
+
worktreesDir: gitService.worktreesDir
|
|
2688
2832
|
});
|
|
2689
2833
|
for (const frame of lastResults.values()) send(frame);
|
|
2690
2834
|
startHeartbeat(sock, opts.heartbeatMs ?? 5e3);
|
|
@@ -2865,7 +3009,7 @@ function probeHub(opts) {
|
|
|
2865
3009
|
|
|
2866
3010
|
// src/cli.ts
|
|
2867
3011
|
import { parseArgs } from "util";
|
|
2868
|
-
var COMMANDS = /* @__PURE__ */ new Set(["start", "run", "doctor", "update"]);
|
|
3012
|
+
var COMMANDS = /* @__PURE__ */ new Set(["start", "run", "service", "doctor", "update"]);
|
|
2869
3013
|
var isCommand = (s) => COMMANDS.has(s);
|
|
2870
3014
|
function parseCli(argv) {
|
|
2871
3015
|
const [first, ...rest] = argv;
|
|
@@ -2884,6 +3028,7 @@ function parseCli(argv) {
|
|
|
2884
3028
|
flagArgs = rest;
|
|
2885
3029
|
}
|
|
2886
3030
|
if (command === "run") return parseRun(flagArgs);
|
|
3031
|
+
if (command === "service") return parseService(flagArgs);
|
|
2887
3032
|
if (command === "update") return parseUpdate(flagArgs);
|
|
2888
3033
|
let values;
|
|
2889
3034
|
try {
|
|
@@ -2942,6 +3087,44 @@ function parseRun(flagArgs) {
|
|
|
2942
3087
|
};
|
|
2943
3088
|
return { kind: "run", flags };
|
|
2944
3089
|
}
|
|
3090
|
+
var SERVICE_ACTIONS = /* @__PURE__ */ new Set(["install", "uninstall"]);
|
|
3091
|
+
var isServiceAction = (s) => SERVICE_ACTIONS.has(s);
|
|
3092
|
+
function parseService(flagArgs) {
|
|
3093
|
+
let values;
|
|
3094
|
+
let positionals;
|
|
3095
|
+
try {
|
|
3096
|
+
({ values, positionals } = parseArgs({
|
|
3097
|
+
args: flagArgs,
|
|
3098
|
+
options: {
|
|
3099
|
+
token: { type: "string" },
|
|
3100
|
+
name: { type: "string" },
|
|
3101
|
+
"hub-url": { type: "string" },
|
|
3102
|
+
help: { type: "boolean", default: false }
|
|
3103
|
+
},
|
|
3104
|
+
allowPositionals: true
|
|
3105
|
+
}));
|
|
3106
|
+
} catch (e) {
|
|
3107
|
+
return { kind: "error", message: e.message };
|
|
3108
|
+
}
|
|
3109
|
+
if (values.help) return { kind: "help", command: "service" };
|
|
3110
|
+
if (positionals.length === 0) {
|
|
3111
|
+
return { kind: "error", message: "service: missing action (`dahrk service install` or `uninstall`)" };
|
|
3112
|
+
}
|
|
3113
|
+
if (positionals.length > 1) {
|
|
3114
|
+
return { kind: "error", message: `service: unexpected argument "${positionals[1]}" (one action at a time)` };
|
|
3115
|
+
}
|
|
3116
|
+
const action = positionals[0];
|
|
3117
|
+
if (!isServiceAction(action)) {
|
|
3118
|
+
return { kind: "error", message: `service: unknown action "${action}" (expected install or uninstall)` };
|
|
3119
|
+
}
|
|
3120
|
+
const flags = {
|
|
3121
|
+
action,
|
|
3122
|
+
...values.token ? { token: values.token } : {},
|
|
3123
|
+
...values.name ? { name: values.name } : {},
|
|
3124
|
+
...values["hub-url"] ? { hubUrl: values["hub-url"] } : {}
|
|
3125
|
+
};
|
|
3126
|
+
return { kind: "service", flags };
|
|
3127
|
+
}
|
|
2945
3128
|
function parseUpdate(flagArgs) {
|
|
2946
3129
|
let values;
|
|
2947
3130
|
try {
|
|
@@ -2989,6 +3172,24 @@ function usage(bin, command) {
|
|
|
2989
3172
|
" --token <token> Enrolment token to verify against the hub (or set DAHRK_ENROL_TOKEN)."
|
|
2990
3173
|
].join("\n");
|
|
2991
3174
|
}
|
|
3175
|
+
if (command === "service") {
|
|
3176
|
+
return [
|
|
3177
|
+
`Usage: ${bin} service install|uninstall [options]`,
|
|
3178
|
+
"",
|
|
3179
|
+
"Install (or remove) the node as an always-on service that starts on boot and restarts on",
|
|
3180
|
+
"failure - a launchd LaunchAgent on macOS, a systemd user service on Linux. No pm2, no root.",
|
|
3181
|
+
"The node id persisted at ~/.dahrk/node.json means it re-attaches as the same node across reboots.",
|
|
3182
|
+
"",
|
|
3183
|
+
"Actions:",
|
|
3184
|
+
" install Generate and register the service, then start it.",
|
|
3185
|
+
" uninstall Stop, deregister, and remove the service.",
|
|
3186
|
+
"",
|
|
3187
|
+
"Options (install; baked into the service, uninstall ignores them):",
|
|
3188
|
+
" --token <token> Enrolment token (required; or set DAHRK_ENROL_TOKEN).",
|
|
3189
|
+
" --hub-url <url> Hub WebSocket URL (or set DAHRK_HUB_URL).",
|
|
3190
|
+
" --name <name> Display-name override (else the hub assigns one)."
|
|
3191
|
+
].join("\n");
|
|
3192
|
+
}
|
|
2992
3193
|
if (command === "doctor") {
|
|
2993
3194
|
return [
|
|
2994
3195
|
`Usage: ${bin} doctor [options]`,
|
|
@@ -3018,6 +3219,7 @@ function usage(bin, command) {
|
|
|
3018
3219
|
"Commands:",
|
|
3019
3220
|
" start Run the edge node (default). Needs a --token and a hub URL.",
|
|
3020
3221
|
" run Run a workflow locally (engine-backed), e.g. `run preflight`.",
|
|
3222
|
+
" service Install/uninstall the node as an always-on service (launchd/systemd).",
|
|
3021
3223
|
" doctor Preflight checks: Node, runtimes, hub reachability, token validity.",
|
|
3022
3224
|
" update Update the client to the latest release (or print how for your channel).",
|
|
3023
3225
|
" version Print the client version.",
|
|
@@ -3374,9 +3576,257 @@ function gatherHostFacts(repoPath) {
|
|
|
3374
3576
|
};
|
|
3375
3577
|
}
|
|
3376
3578
|
|
|
3377
|
-
// src/
|
|
3579
|
+
// src/service.ts
|
|
3378
3580
|
import { execFileSync as execFileSync5 } from "child_process";
|
|
3379
|
-
import { realpathSync } from "fs";
|
|
3581
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync6, realpathSync, rmSync as rmSync4, writeFileSync as writeFileSync6 } from "fs";
|
|
3582
|
+
import { homedir as homedir3, platform as osPlatform3, userInfo } from "os";
|
|
3583
|
+
import { join as join8 } from "path";
|
|
3584
|
+
var LAUNCHD_LABEL = "ai.dahrk.node";
|
|
3585
|
+
var SYSTEMD_UNIT = "dahrk-node.service";
|
|
3586
|
+
function detectManager(plat) {
|
|
3587
|
+
if (plat === "darwin") return "launchd";
|
|
3588
|
+
if (plat === "linux") return "systemd";
|
|
3589
|
+
return "unsupported";
|
|
3590
|
+
}
|
|
3591
|
+
function serviceEnv(inputs) {
|
|
3592
|
+
return {
|
|
3593
|
+
DAHRK_ENROL_TOKEN: inputs.token,
|
|
3594
|
+
...inputs.hubUrl ? { DAHRK_HUB_URL: inputs.hubUrl } : {},
|
|
3595
|
+
...inputs.name ? { DAHRK_NODE_NAME: inputs.name } : {},
|
|
3596
|
+
...inputs.pathEnv ? { PATH: inputs.pathEnv } : {}
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
function xmlEscape(s) {
|
|
3600
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3601
|
+
}
|
|
3602
|
+
function renderLaunchdPlist(inputs) {
|
|
3603
|
+
const argv = [inputs.nodeBin, inputs.scriptPath, "start"];
|
|
3604
|
+
const env = serviceEnv(inputs);
|
|
3605
|
+
const progArgs = argv.map((a) => ` <string>${xmlEscape(a)}</string>`).join("\n");
|
|
3606
|
+
const envEntries = Object.entries(env).map(([k, v]) => ` <key>${xmlEscape(k)}</key>
|
|
3607
|
+
<string>${xmlEscape(v)}</string>`).join("\n");
|
|
3608
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
3609
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3610
|
+
<plist version="1.0">
|
|
3611
|
+
<dict>
|
|
3612
|
+
<key>Label</key>
|
|
3613
|
+
<string>${LAUNCHD_LABEL}</string>
|
|
3614
|
+
<key>ProgramArguments</key>
|
|
3615
|
+
<array>
|
|
3616
|
+
${progArgs}
|
|
3617
|
+
</array>
|
|
3618
|
+
<key>EnvironmentVariables</key>
|
|
3619
|
+
<dict>
|
|
3620
|
+
${envEntries}
|
|
3621
|
+
</dict>
|
|
3622
|
+
<key>WorkingDirectory</key>
|
|
3623
|
+
<string>${xmlEscape(inputs.homeDir)}</string>
|
|
3624
|
+
<key>RunAtLoad</key>
|
|
3625
|
+
<true/>
|
|
3626
|
+
<key>KeepAlive</key>
|
|
3627
|
+
<true/>
|
|
3628
|
+
<key>ThrottleInterval</key>
|
|
3629
|
+
<integer>10</integer>
|
|
3630
|
+
<key>StandardOutPath</key>
|
|
3631
|
+
<string>${xmlEscape(join8(inputs.logDir, "node.out.log"))}</string>
|
|
3632
|
+
<key>StandardErrorPath</key>
|
|
3633
|
+
<string>${xmlEscape(join8(inputs.logDir, "node.err.log"))}</string>
|
|
3634
|
+
</dict>
|
|
3635
|
+
</plist>
|
|
3636
|
+
`;
|
|
3637
|
+
}
|
|
3638
|
+
function systemdEnvValue(v) {
|
|
3639
|
+
return /\s/.test(v) ? `"${v.replace(/"/g, '\\"')}"` : v;
|
|
3640
|
+
}
|
|
3641
|
+
function renderSystemdUnit(inputs) {
|
|
3642
|
+
const exec = [inputs.nodeBin, inputs.scriptPath, "start"].map((a) => /\s/.test(a) ? `"${a}"` : a).join(" ");
|
|
3643
|
+
const env = serviceEnv(inputs);
|
|
3644
|
+
const envLines = Object.entries(env).map(([k, v]) => `Environment=${k}=${systemdEnvValue(v)}`).join("\n");
|
|
3645
|
+
return `[Unit]
|
|
3646
|
+
Description=Dahrk node (self-managed edge node)
|
|
3647
|
+
Documentation=https://dahrk.ai/docs
|
|
3648
|
+
After=network-online.target
|
|
3649
|
+
Wants=network-online.target
|
|
3650
|
+
|
|
3651
|
+
[Service]
|
|
3652
|
+
Type=simple
|
|
3653
|
+
ExecStart=${exec}
|
|
3654
|
+
${envLines}
|
|
3655
|
+
WorkingDirectory=${inputs.homeDir}
|
|
3656
|
+
Restart=on-failure
|
|
3657
|
+
RestartSec=3
|
|
3658
|
+
RestartPreventExitStatus=78
|
|
3659
|
+
|
|
3660
|
+
[Install]
|
|
3661
|
+
WantedBy=default.target
|
|
3662
|
+
`;
|
|
3663
|
+
}
|
|
3664
|
+
function buildPlan(inputs) {
|
|
3665
|
+
if (inputs.manager === "launchd") {
|
|
3666
|
+
const filePath2 = join8(inputs.homeDir, "Library", "LaunchAgents", `${LAUNCHD_LABEL}.plist`);
|
|
3667
|
+
return {
|
|
3668
|
+
manager: "launchd",
|
|
3669
|
+
label: LAUNCHD_LABEL,
|
|
3670
|
+
filePath: filePath2,
|
|
3671
|
+
content: renderLaunchdPlist(inputs),
|
|
3672
|
+
// Unload first so a re-install picks up the rewritten plist; a not-loaded unload is a no-op.
|
|
3673
|
+
installCommands: [
|
|
3674
|
+
{ argv: ["launchctl", "unload", filePath2], ignoreFailure: true },
|
|
3675
|
+
{ argv: ["launchctl", "load", "-w", filePath2] }
|
|
3676
|
+
],
|
|
3677
|
+
uninstallCommands: [{ argv: ["launchctl", "unload", "-w", filePath2], ignoreFailure: true }],
|
|
3678
|
+
logHint: `tail -f ${join8(inputs.logDir, "node.err.log")}`
|
|
3679
|
+
};
|
|
3680
|
+
}
|
|
3681
|
+
const filePath = join8(inputs.homeDir, ".config", "systemd", "user", SYSTEMD_UNIT);
|
|
3682
|
+
const user = userInfo().username;
|
|
3683
|
+
return {
|
|
3684
|
+
manager: "systemd",
|
|
3685
|
+
label: SYSTEMD_UNIT,
|
|
3686
|
+
filePath,
|
|
3687
|
+
content: renderSystemdUnit(inputs),
|
|
3688
|
+
// Reload to see the new unit, enable+start it, then enable linger so it survives logout / boots
|
|
3689
|
+
// headless. Linger can be denied without privilege on some hosts, so it is best-effort.
|
|
3690
|
+
installCommands: [
|
|
3691
|
+
{ argv: ["systemctl", "--user", "daemon-reload"] },
|
|
3692
|
+
{ argv: ["systemctl", "--user", "enable", "--now", SYSTEMD_UNIT] },
|
|
3693
|
+
{ argv: ["loginctl", "enable-linger", user], ignoreFailure: true }
|
|
3694
|
+
],
|
|
3695
|
+
uninstallCommands: [
|
|
3696
|
+
{ argv: ["systemctl", "--user", "disable", "--now", SYSTEMD_UNIT], ignoreFailure: true },
|
|
3697
|
+
{ argv: ["systemctl", "--user", "daemon-reload"], ignoreFailure: true }
|
|
3698
|
+
],
|
|
3699
|
+
logHint: `journalctl --user -u ${SYSTEMD_UNIT} -f`
|
|
3700
|
+
};
|
|
3701
|
+
}
|
|
3702
|
+
function printUnsupported(out) {
|
|
3703
|
+
out("dahrk service is supported on macOS (launchd) and Linux (systemd) only.");
|
|
3704
|
+
out("On other platforms, run the node under a process manager such as pm2 (see the README).");
|
|
3705
|
+
return 1;
|
|
3706
|
+
}
|
|
3707
|
+
function runCommands(commands, d) {
|
|
3708
|
+
for (const cmd of commands) {
|
|
3709
|
+
const code = d.run(cmd.argv);
|
|
3710
|
+
if (code !== 0 && !cmd.ignoreFailure) {
|
|
3711
|
+
d.out(` command failed (${code}): ${cmd.argv.join(" ")}`);
|
|
3712
|
+
return code;
|
|
3713
|
+
}
|
|
3714
|
+
}
|
|
3715
|
+
return 0;
|
|
3716
|
+
}
|
|
3717
|
+
async function runServiceInstall(inputs, deps = {}) {
|
|
3718
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
3719
|
+
d.out("dahrk service install");
|
|
3720
|
+
d.out("");
|
|
3721
|
+
const manager = detectManager(d.platform);
|
|
3722
|
+
if (manager === "unsupported") return printUnsupported(d.out);
|
|
3723
|
+
if (!inputs.token) {
|
|
3724
|
+
d.out("No enrolment token: pass --token <token> or set DAHRK_ENROL_TOKEN.");
|
|
3725
|
+
d.out("Get one at https://app.dahrk.ai.");
|
|
3726
|
+
return 2;
|
|
3727
|
+
}
|
|
3728
|
+
const plan = buildPlan({
|
|
3729
|
+
manager,
|
|
3730
|
+
nodeBin: d.nodeBin,
|
|
3731
|
+
scriptPath: d.scriptPath,
|
|
3732
|
+
token: inputs.token,
|
|
3733
|
+
...inputs.name ? { name: inputs.name } : {},
|
|
3734
|
+
...inputs.hubUrl ? { hubUrl: inputs.hubUrl } : {},
|
|
3735
|
+
...d.pathEnv ? { pathEnv: d.pathEnv } : {},
|
|
3736
|
+
homeDir: d.homeDir,
|
|
3737
|
+
logDir: d.logDir
|
|
3738
|
+
});
|
|
3739
|
+
try {
|
|
3740
|
+
if (manager === "launchd") d.mkdirp(d.logDir);
|
|
3741
|
+
d.mkdirp(dirOf(plan.filePath));
|
|
3742
|
+
d.writeFile(plan.filePath, plan.content);
|
|
3743
|
+
} catch (e) {
|
|
3744
|
+
d.out(`Could not write the service file at ${plan.filePath}: ${e.message}`);
|
|
3745
|
+
return 1;
|
|
3746
|
+
}
|
|
3747
|
+
d.out(`Wrote ${plan.manager} service: ${plan.filePath}`);
|
|
3748
|
+
const code = runCommands(plan.installCommands, d);
|
|
3749
|
+
d.out("");
|
|
3750
|
+
if (code !== 0) {
|
|
3751
|
+
d.out(`The service file is in place but registering it failed (exit ${code}).`);
|
|
3752
|
+
d.out("Fix the reported error and re-run `dahrk service install`, or load it yourself.");
|
|
3753
|
+
return code;
|
|
3754
|
+
}
|
|
3755
|
+
d.out("Installed. The node will start on boot and restart on failure.");
|
|
3756
|
+
d.out(` logs: ${plan.logHint}`);
|
|
3757
|
+
d.out(" uninstall: dahrk service uninstall");
|
|
3758
|
+
return 0;
|
|
3759
|
+
}
|
|
3760
|
+
async function runServiceUninstall(deps = {}) {
|
|
3761
|
+
const d = { ...defaultDeps3(), ...deps };
|
|
3762
|
+
d.out("dahrk service uninstall");
|
|
3763
|
+
d.out("");
|
|
3764
|
+
const manager = detectManager(d.platform);
|
|
3765
|
+
if (manager === "unsupported") return printUnsupported(d.out);
|
|
3766
|
+
const plan = buildPlan({
|
|
3767
|
+
manager,
|
|
3768
|
+
nodeBin: d.nodeBin,
|
|
3769
|
+
scriptPath: d.scriptPath,
|
|
3770
|
+
token: "-",
|
|
3771
|
+
homeDir: d.homeDir,
|
|
3772
|
+
logDir: d.logDir
|
|
3773
|
+
});
|
|
3774
|
+
if (!d.fileExists(plan.filePath)) {
|
|
3775
|
+
d.out(`No service installed (${plan.filePath} not found). Nothing to do.`);
|
|
3776
|
+
return 0;
|
|
3777
|
+
}
|
|
3778
|
+
runCommands(plan.uninstallCommands, d);
|
|
3779
|
+
try {
|
|
3780
|
+
d.removeFile(plan.filePath);
|
|
3781
|
+
} catch (e) {
|
|
3782
|
+
d.out(`Deregistered, but could not delete ${plan.filePath}: ${e.message}`);
|
|
3783
|
+
return 1;
|
|
3784
|
+
}
|
|
3785
|
+
d.out(`Removed ${plan.filePath}. The node will no longer start on boot.`);
|
|
3786
|
+
return 0;
|
|
3787
|
+
}
|
|
3788
|
+
function dirOf(path) {
|
|
3789
|
+
const i = path.lastIndexOf("/");
|
|
3790
|
+
return i <= 0 ? path : path.slice(0, i);
|
|
3791
|
+
}
|
|
3792
|
+
function resolveScriptPath() {
|
|
3793
|
+
const argv1 = process.argv[1] ?? "";
|
|
3794
|
+
try {
|
|
3795
|
+
return realpathSync(argv1);
|
|
3796
|
+
} catch {
|
|
3797
|
+
return argv1;
|
|
3798
|
+
}
|
|
3799
|
+
}
|
|
3800
|
+
var defaultDeps3 = () => ({
|
|
3801
|
+
platform: osPlatform3(),
|
|
3802
|
+
homeDir: homedir3(),
|
|
3803
|
+
nodeBin: process.execPath,
|
|
3804
|
+
scriptPath: resolveScriptPath(),
|
|
3805
|
+
logDir: join8(process.env.DAHRK_STATE_DIR ?? join8(homedir3(), ".dahrk"), "logs"),
|
|
3806
|
+
// Snapshot the operator's PATH at install time so the daemon finds git + the runtime CLIs (Homebrew /
|
|
3807
|
+
// npm-global bins) that a supervisor's minimal PATH would otherwise hide.
|
|
3808
|
+
pathEnv: process.env.PATH,
|
|
3809
|
+
mkdirp: (dir) => void mkdirSync6(dir, { recursive: true }),
|
|
3810
|
+
writeFile: (path, content) => writeFileSync6(path, content),
|
|
3811
|
+
removeFile: (path) => rmSync4(path, { force: true }),
|
|
3812
|
+
fileExists: (path) => existsSync5(path),
|
|
3813
|
+
run: (argv) => {
|
|
3814
|
+
const [cmd, ...args] = argv;
|
|
3815
|
+
try {
|
|
3816
|
+
execFileSync5(cmd, args, { stdio: "inherit" });
|
|
3817
|
+
return 0;
|
|
3818
|
+
} catch (e) {
|
|
3819
|
+
const status = e.status;
|
|
3820
|
+
return typeof status === "number" ? status : 1;
|
|
3821
|
+
}
|
|
3822
|
+
},
|
|
3823
|
+
out: (line) => void process.stdout.write(`${line}
|
|
3824
|
+
`)
|
|
3825
|
+
});
|
|
3826
|
+
|
|
3827
|
+
// src/update.ts
|
|
3828
|
+
import { execFileSync as execFileSync6 } from "child_process";
|
|
3829
|
+
import { realpathSync as realpathSync2 } from "fs";
|
|
3380
3830
|
var LATEST_URL = "https://registry.npmjs.org/dahrk-node/latest";
|
|
3381
3831
|
var CHANNEL_COMMANDS = {
|
|
3382
3832
|
npm: "npm install -g dahrk-node@latest",
|
|
@@ -3397,7 +3847,7 @@ function detectChannel(binPath) {
|
|
|
3397
3847
|
if (!binPath) return "unknown";
|
|
3398
3848
|
let resolved = binPath;
|
|
3399
3849
|
try {
|
|
3400
|
-
resolved =
|
|
3850
|
+
resolved = realpathSync2(binPath);
|
|
3401
3851
|
} catch {
|
|
3402
3852
|
}
|
|
3403
3853
|
if (/[\\/]node_modules[\\/]dahrk-node[\\/]/.test(resolved)) return "npm";
|
|
@@ -3425,7 +3875,7 @@ function printChannelCommands(out) {
|
|
|
3425
3875
|
out(` curl: ${CHANNEL_COMMANDS.curl}`);
|
|
3426
3876
|
}
|
|
3427
3877
|
async function runUpdate(inputs, deps = {}) {
|
|
3428
|
-
const d = { ...
|
|
3878
|
+
const d = { ...defaultDeps4(), ...deps };
|
|
3429
3879
|
const current = inputs.currentVersion;
|
|
3430
3880
|
d.out("dahrk update");
|
|
3431
3881
|
d.out("");
|
|
@@ -3482,14 +3932,14 @@ async function fetchLatestVersion() {
|
|
|
3482
3932
|
function spawnUpgrade(argv) {
|
|
3483
3933
|
const [cmd, ...args] = argv;
|
|
3484
3934
|
try {
|
|
3485
|
-
|
|
3935
|
+
execFileSync6(cmd, args, { stdio: "inherit" });
|
|
3486
3936
|
return 0;
|
|
3487
3937
|
} catch (e) {
|
|
3488
3938
|
const status = e.status;
|
|
3489
3939
|
return typeof status === "number" ? status : 1;
|
|
3490
3940
|
}
|
|
3491
3941
|
}
|
|
3492
|
-
var
|
|
3942
|
+
var defaultDeps4 = () => ({
|
|
3493
3943
|
binPath: process.argv[1],
|
|
3494
3944
|
fetchLatest: fetchLatestVersion,
|
|
3495
3945
|
runUpgrade: spawnUpgrade,
|
|
@@ -3498,19 +3948,19 @@ var defaultDeps3 = () => ({
|
|
|
3498
3948
|
});
|
|
3499
3949
|
|
|
3500
3950
|
// src/main.ts
|
|
3501
|
-
var CLIENT_VERSION = "0.1.
|
|
3951
|
+
var CLIENT_VERSION = "0.1.5";
|
|
3502
3952
|
var DEFAULT_HUB_URL = "wss://api.dahrk.ai";
|
|
3503
3953
|
var list = (v) => (v ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
3504
3954
|
var RUNTIMES = ["claude-code", "codex", "pi"];
|
|
3505
3955
|
var isRuntime = (r) => RUNTIMES.includes(r);
|
|
3506
3956
|
function stateDir(env) {
|
|
3507
|
-
return env.DAHRK_STATE_DIR ??
|
|
3957
|
+
return env.DAHRK_STATE_DIR ?? join9(homedir4(), ".dahrk");
|
|
3508
3958
|
}
|
|
3509
3959
|
function legacyStateDir(env) {
|
|
3510
|
-
return env.DAHRK_STATE_DIR ? void 0 :
|
|
3960
|
+
return env.DAHRK_STATE_DIR ? void 0 : join9(homedir4(), ".skakel");
|
|
3511
3961
|
}
|
|
3512
3962
|
function readNodeId(file) {
|
|
3513
|
-
if (!
|
|
3963
|
+
if (!existsSync6(file)) return void 0;
|
|
3514
3964
|
try {
|
|
3515
3965
|
const parsed = JSON.parse(readFileSync5(file, "utf8"));
|
|
3516
3966
|
if (typeof parsed.nodeId === "string" && parsed.nodeId) return parsed.nodeId;
|
|
@@ -3522,18 +3972,18 @@ function resolveNodeId(env, opts = {}) {
|
|
|
3522
3972
|
if (env.DAHRK_NODE_ID) return env.DAHRK_NODE_ID;
|
|
3523
3973
|
if (opts.ephemeral) return randomUUID3();
|
|
3524
3974
|
const dir = stateDir(env);
|
|
3525
|
-
const file =
|
|
3975
|
+
const file = join9(dir, "node.json");
|
|
3526
3976
|
const existing = readNodeId(file);
|
|
3527
3977
|
if (existing) return existing;
|
|
3528
3978
|
const legacy = legacyStateDir(env);
|
|
3529
3979
|
if (legacy) {
|
|
3530
|
-
const legacyId = readNodeId(
|
|
3980
|
+
const legacyId = readNodeId(join9(legacy, "node.json"));
|
|
3531
3981
|
if (legacyId) return legacyId;
|
|
3532
3982
|
}
|
|
3533
3983
|
const nodeId = randomUUID3();
|
|
3534
3984
|
try {
|
|
3535
|
-
|
|
3536
|
-
|
|
3985
|
+
mkdirSync7(dir, { recursive: true });
|
|
3986
|
+
writeFileSync7(file, `${JSON.stringify({ nodeId }, null, 2)}
|
|
3537
3987
|
`);
|
|
3538
3988
|
} catch (e) {
|
|
3539
3989
|
console.warn(`could not persist node id to ${file}: ${e.message}`);
|
|
@@ -3659,6 +4109,22 @@ async function main() {
|
|
|
3659
4109
|
case "run":
|
|
3660
4110
|
process.exitCode = await runWorkflow(parsed.flags);
|
|
3661
4111
|
break;
|
|
4112
|
+
case "service": {
|
|
4113
|
+
if (parsed.flags.action === "uninstall") {
|
|
4114
|
+
process.exitCode = await runServiceUninstall();
|
|
4115
|
+
break;
|
|
4116
|
+
}
|
|
4117
|
+
const env = applyEnvAliases(process.env);
|
|
4118
|
+
const token = parsed.flags.token ?? env.DAHRK_ENROL_TOKEN;
|
|
4119
|
+
const name = parsed.flags.name ?? env.DAHRK_NODE_NAME;
|
|
4120
|
+
const hubUrl = parsed.flags.hubUrl ?? env.DAHRK_HUB_URL;
|
|
4121
|
+
process.exitCode = await runServiceInstall({
|
|
4122
|
+
...token ? { token } : {},
|
|
4123
|
+
...name ? { name } : {},
|
|
4124
|
+
...hubUrl ? { hubUrl } : {}
|
|
4125
|
+
});
|
|
4126
|
+
break;
|
|
4127
|
+
}
|
|
3662
4128
|
case "update":
|
|
3663
4129
|
process.exitCode = await runUpdate({ currentVersion: CLIENT_VERSION, check: parsed.flags.check });
|
|
3664
4130
|
break;
|
|
@@ -3671,7 +4137,7 @@ var invokedAsEntrypoint = (() => {
|
|
|
3671
4137
|
const argv1 = process.argv[1];
|
|
3672
4138
|
if (!argv1) return false;
|
|
3673
4139
|
try {
|
|
3674
|
-
return pathToFileURL(
|
|
4140
|
+
return pathToFileURL(realpathSync3(argv1)).href === import.meta.url;
|
|
3675
4141
|
} catch {
|
|
3676
4142
|
return false;
|
|
3677
4143
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dahrk-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"exports": "./dist/main.js",
|
|
35
35
|
"dependencies": {
|
|
36
36
|
"@anthropic-ai/claude-agent-sdk": "0.3.183",
|
|
37
|
-
"@dahrk/contracts": "^0.1.
|
|
37
|
+
"@dahrk/contracts": "^0.1.1",
|
|
38
38
|
"@openai/codex-sdk": "0.141.0",
|
|
39
39
|
"ws": "^8.18.0",
|
|
40
40
|
"zod": "^3.23.8"
|