@sagentlab/navarch-runtime 0.1.1 → 0.1.3
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/README.md +123 -104
- package/dist/adapters/claude.cjs +13 -1
- package/dist/adapters/codex.cjs +73 -25
- package/dist/api.cjs +7 -12
- package/dist/claim-loop.cjs +7 -1
- package/dist/cli.cjs +38 -14
- package/dist/config.cjs +13 -4
- package/dist/exit-conditions.cjs +39 -21
- package/dist/git-worktree.cjs +145 -0
- package/dist/github-pr.cjs +87 -0
- package/dist/machine-store.cjs +3 -0
- package/dist/prompt.cjs +25 -1
- package/dist/sandbox.cjs +20 -6
- package/dist/session.cjs +181 -37
- package/dist/types.cjs +3 -10
- package/package.json +6 -6
package/dist/session.cjs
CHANGED
|
@@ -14,14 +14,16 @@ const upload_cjs_1 = require("./upload.cjs");
|
|
|
14
14
|
const prompt_cjs_1 = require("./prompt.cjs");
|
|
15
15
|
const mcp_config_cjs_1 = require("./mcp-config.cjs");
|
|
16
16
|
const logger_cjs_1 = require("./logger.cjs");
|
|
17
|
-
|
|
17
|
+
const git_worktree_cjs_1 = require("./git-worktree.cjs");
|
|
18
|
+
const github_pr_cjs_1 = require("./github-pr.cjs");
|
|
19
|
+
/** Filename the generated platform MCP config is written under inside the session metadata directory. */
|
|
18
20
|
const MCP_CONFIG_FILENAME = "mcp-config.json";
|
|
19
21
|
const log = (0, logger_cjs_1.createLogger)("session");
|
|
20
22
|
/**
|
|
21
23
|
* Runs one claimed task end to end (implementation-plan.md WP-07):
|
|
22
24
|
* 1. write the prompt file
|
|
23
25
|
* 2. fetch secrets from the broker once, at session start
|
|
24
|
-
* 3. stand up a Docker sandbox
|
|
26
|
+
* 3. optionally stand up a Docker sandbox when explicitly configured
|
|
25
27
|
* 4. run the configured agent adapter (Claude Code or Codex, per
|
|
26
28
|
* NAVARCH_AGENT — adapters/index.cts#selectAdapter), heartbeating the
|
|
27
29
|
* lease throughout
|
|
@@ -29,19 +31,29 @@ const log = (0, logger_cjs_1.createLogger)("session");
|
|
|
29
31
|
* lease (recording which agent_type ran it)
|
|
30
32
|
* 6. wipe the sandbox unconditionally
|
|
31
33
|
*
|
|
32
|
-
* NEEDS LIVE VERIFICATION: the full path requires a real
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* NEEDS LIVE VERIFICATION: the full path requires a real `claude` (or
|
|
35
|
+
* `codex`) binary and a live control-plane API; Docker-backed execution also
|
|
36
|
+
* requires a real Docker daemon. Unit tests exercise each collaborator
|
|
37
|
+
* (api.cts, sandbox.cts, exit-conditions.cts, redact.cts, adapters/*.cts) in
|
|
38
|
+
* isolation instead; see runtime/README.md.
|
|
37
39
|
*/
|
|
38
40
|
async function runSession(deps, claimed, sessionId) {
|
|
39
41
|
const { api, config } = deps;
|
|
40
42
|
const { lease_id: leaseId, task, context_bundle: bundle } = claimed;
|
|
43
|
+
const execution = bundle.execution ?? {
|
|
44
|
+
profile: task.execution_profile ?? "standard",
|
|
45
|
+
model: config.agentType === "codex" ? "gpt-5.6" : "best",
|
|
46
|
+
reasoning_effort: "medium",
|
|
47
|
+
};
|
|
48
|
+
const executionReport = {
|
|
49
|
+
model: execution.model,
|
|
50
|
+
execution_profile: execution.profile,
|
|
51
|
+
reasoning_effort: execution.reasoning_effort,
|
|
52
|
+
};
|
|
41
53
|
// The session's identity is the pre-allocated session id sent at claim time
|
|
42
54
|
// (recorded on the lease by the dispatcher). Lease-scoped API calls
|
|
43
55
|
// (heartbeat/complete/issue/transcript) still key on leaseId.
|
|
44
|
-
const workDir = node_path_1.default.join(config.workspaceRoot, sessionId);
|
|
56
|
+
const workDir = node_path_1.default.join(config.workspaceRoot, "sessions", sessionId);
|
|
45
57
|
await node_fs_1.promises.mkdir(workDir, { recursive: true });
|
|
46
58
|
const promptText = (0, prompt_cjs_1.renderPrompt)(task, bundle);
|
|
47
59
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), promptText, "utf8");
|
|
@@ -56,15 +68,72 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
56
68
|
secrets = issued.secrets;
|
|
57
69
|
registry.registerAll(secrets);
|
|
58
70
|
}
|
|
59
|
-
const
|
|
71
|
+
const cloneUrl = bundle.repository?.clone_url ??
|
|
72
|
+
(task.repo ? `https://github.com/${task.repo.replace(/\.git$/, "")}.git` : null);
|
|
73
|
+
if (!cloneUrl) {
|
|
74
|
+
const failureSummary = `Project ${task.project_id} has no GitHub repository URL. Set it in Project settings before dispatching work.`;
|
|
75
|
+
await api.completeLease(leaseId, {
|
|
76
|
+
status: "failed",
|
|
77
|
+
report: failureSummary,
|
|
78
|
+
failure_summary: failureSummary,
|
|
79
|
+
evidence_urls: [],
|
|
80
|
+
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
81
|
+
exit_status: "crashed",
|
|
82
|
+
agent_type: config.agentType,
|
|
83
|
+
...executionReport,
|
|
84
|
+
});
|
|
85
|
+
secrets = {};
|
|
86
|
+
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const githubToken = secrets[bundle.repository?.credential_secret_name ?? "github-pat"];
|
|
90
|
+
const gitWorktree = new git_worktree_cjs_1.GitWorktree({
|
|
91
|
+
workspaceRoot: config.workspaceRoot,
|
|
92
|
+
projectId: task.project_id,
|
|
93
|
+
taskId: task.id,
|
|
94
|
+
sessionId,
|
|
95
|
+
cloneUrl,
|
|
96
|
+
githubToken,
|
|
97
|
+
});
|
|
98
|
+
const knownGuidanceIds = new Set((bundle.guidance ?? []).map((entry) => entry.id));
|
|
99
|
+
const deliveredGuidance = [...(bundle.guidance ?? [])];
|
|
100
|
+
let guidanceCursor = bundle.guidance_cursor ?? null;
|
|
101
|
+
let pendingGuidance = [];
|
|
102
|
+
let activeAbortController = null;
|
|
60
103
|
let leaseLost = false;
|
|
61
|
-
|
|
62
|
-
|
|
104
|
+
let heartbeatInFlight = null;
|
|
105
|
+
const pollLease = () => {
|
|
106
|
+
if (heartbeatInFlight)
|
|
107
|
+
return heartbeatInFlight;
|
|
108
|
+
const request = api
|
|
109
|
+
.heartbeatLease(leaseId, { guidance_after: guidanceCursor })
|
|
110
|
+
.then((heartbeat) => {
|
|
111
|
+
guidanceCursor = heartbeat.guidance_cursor ?? guidanceCursor;
|
|
112
|
+
const fresh = (heartbeat.guidance ?? []).filter((entry) => {
|
|
113
|
+
if (knownGuidanceIds.has(entry.id))
|
|
114
|
+
return false;
|
|
115
|
+
knownGuidanceIds.add(entry.id);
|
|
116
|
+
return true;
|
|
117
|
+
});
|
|
118
|
+
if (fresh.length > 0) {
|
|
119
|
+
pendingGuidance.push(...fresh);
|
|
120
|
+
deliveredGuidance.push(...fresh);
|
|
121
|
+
log.info(`received ${fresh.length} guidance update${fresh.length === 1 ? "" : "s"} for ${leaseId}; restarting the agent turn in the same worktree.`);
|
|
122
|
+
activeAbortController?.abort();
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
.catch((err) => {
|
|
63
126
|
log.warn(`lease heartbeat failed for ${leaseId}: ${String(err)} — killing session.`);
|
|
64
127
|
leaseLost = true;
|
|
65
|
-
|
|
128
|
+
activeAbortController?.abort();
|
|
129
|
+
})
|
|
130
|
+
.finally(() => {
|
|
131
|
+
heartbeatInFlight = null;
|
|
66
132
|
});
|
|
67
|
-
|
|
133
|
+
heartbeatInFlight = request;
|
|
134
|
+
return request;
|
|
135
|
+
};
|
|
136
|
+
const heartbeatTimer = setInterval(() => void pollLease(), config.leaseHeartbeatIntervalMs);
|
|
68
137
|
const dockerAvailable = config.sandboxMode === "docker" && (await (0, sandbox_cjs_1.isDockerAvailable)());
|
|
69
138
|
if (config.sandboxMode === "docker" && !dockerAvailable) {
|
|
70
139
|
clearInterval(heartbeatTimer);
|
|
@@ -73,16 +142,26 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
73
142
|
.completeLease(leaseId, {
|
|
74
143
|
status: "failed",
|
|
75
144
|
report: "Docker sandbox unavailable on this machine.",
|
|
145
|
+
failure_summary: "Docker sandbox unavailable on this machine.",
|
|
76
146
|
evidence_urls: [],
|
|
77
147
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
78
148
|
exit_status: "crashed",
|
|
79
149
|
agent_type: config.agentType,
|
|
150
|
+
...executionReport,
|
|
80
151
|
})
|
|
81
152
|
.catch((err) => log.warn(`complete() after docker-unavailable also failed: ${String(err)}`));
|
|
153
|
+
secrets = {};
|
|
154
|
+
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
82
155
|
return;
|
|
83
156
|
}
|
|
84
157
|
const sandbox = dockerAvailable
|
|
85
|
-
? new sandbox_cjs_1.DockerSandbox({
|
|
158
|
+
? new sandbox_cjs_1.DockerSandbox({
|
|
159
|
+
sessionId,
|
|
160
|
+
workspaceRoot: node_path_1.default.join(config.workspaceRoot, "sessions"),
|
|
161
|
+
image: config.dockerImage,
|
|
162
|
+
containerWorkDir: gitWorktree.worktreePath,
|
|
163
|
+
sharedGitDir: gitWorktree.repositoryPath,
|
|
164
|
+
})
|
|
86
165
|
: null;
|
|
87
166
|
// Platform MCP config (implementation-plan.md WP-07: "--mcp-config
|
|
88
167
|
// pointing at the platform MCP server"): generated fresh per session,
|
|
@@ -90,8 +169,8 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
90
169
|
// auth app/api/mcp/route.ts requires -- see lib/navarch/mcp/context.ts),
|
|
91
170
|
// unless the operator pinned a static override via NAVARCH_MCP_CONFIG_PATH
|
|
92
171
|
// (e.g. pointing at a fake MCP server in local testing). Written to
|
|
93
|
-
// workDir (host path)
|
|
94
|
-
//
|
|
172
|
+
// workDir (host path), which Docker mode mounts at that same absolute path
|
|
173
|
+
// so worktree .git pointers and this config path remain valid --
|
|
95
174
|
// the selected adapter (adapters/claude.cts or adapters/codex.cts) needs a
|
|
96
175
|
// path valid in whichever environment it actually runs.
|
|
97
176
|
let mcpConfigPath = config.mcpConfigPath;
|
|
@@ -102,13 +181,13 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
102
181
|
leaseId,
|
|
103
182
|
});
|
|
104
183
|
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, MCP_CONFIG_FILENAME), JSON.stringify(mcpConfig, null, 2), "utf8");
|
|
105
|
-
mcpConfigPath =
|
|
184
|
+
mcpConfigPath = node_path_1.default.join(workDir, MCP_CONFIG_FILENAME);
|
|
106
185
|
}
|
|
107
186
|
try {
|
|
187
|
+
await gitWorktree.prepare();
|
|
108
188
|
if (sandbox) {
|
|
109
189
|
await sandbox.create();
|
|
110
190
|
await sandbox.injectEnv(toEnvMap(secrets));
|
|
111
|
-
await sandbox.cloneRepo(task.repo, Boolean(secrets["github-pat"]));
|
|
112
191
|
}
|
|
113
192
|
// Picks the Claude Code or Codex adapter per NAVARCH_AGENT
|
|
114
193
|
// (config.cts's `agentType`) — see adapters/index.cts#selectAdapter.
|
|
@@ -118,27 +197,78 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
118
197
|
const adapter = (0, index_cjs_1.selectAdapter)(config.agentType);
|
|
119
198
|
const bin = config.agentType === "codex" ? config.codexBin : config.claudeBin;
|
|
120
199
|
const extraArgs = config.agentType === "codex" ? config.codexExtraArgs : config.claudeExtraArgs;
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
200
|
+
const attempts = [];
|
|
201
|
+
let result;
|
|
202
|
+
while (true) {
|
|
203
|
+
// Guidance can arrive while the worktree/sandbox is being prepared.
|
|
204
|
+
// It is already included in deliveredGuidance, so clear the pending
|
|
205
|
+
// notification and start the first turn with the corrected prompt.
|
|
206
|
+
pendingGuidance = [];
|
|
207
|
+
activeAbortController = new AbortController();
|
|
208
|
+
if (leaseLost)
|
|
209
|
+
activeAbortController.abort();
|
|
210
|
+
const runPrompt = deliveredGuidance.length > (bundle.guidance?.length ?? 0)
|
|
211
|
+
? (0, prompt_cjs_1.renderGuidanceCorrectionPrompt)(promptText, deliveredGuidance)
|
|
212
|
+
: promptText;
|
|
213
|
+
await node_fs_1.promises.writeFile(node_path_1.default.join(workDir, "prompt.md"), runPrompt, "utf8");
|
|
214
|
+
const turnResult = await adapter.run({
|
|
215
|
+
prompt: runPrompt,
|
|
216
|
+
mcpConfigPath,
|
|
217
|
+
bin,
|
|
218
|
+
extraArgs,
|
|
219
|
+
model: execution.model,
|
|
220
|
+
reasoningEffort: execution.reasoning_effort,
|
|
221
|
+
timeoutMs: config.sessionTimeoutMs,
|
|
222
|
+
env: toEnvMap(secrets),
|
|
223
|
+
cwd: sandbox ? undefined : gitWorktree.worktreePath,
|
|
224
|
+
dockerExec: sandbox ? { containerName: sandbox.name, runner: sandbox_cjs_1.nodeCommandRunner } : undefined,
|
|
225
|
+
signal: activeAbortController.signal,
|
|
226
|
+
});
|
|
227
|
+
activeAbortController = null;
|
|
228
|
+
attempts.push(turnResult);
|
|
229
|
+
// Close the small race between a naturally completed turn and the next
|
|
230
|
+
// scheduled heartbeat. If guidance landed, run another turn before the
|
|
231
|
+
// lease can be completed.
|
|
232
|
+
await pollLease();
|
|
233
|
+
if (!leaseLost && pendingGuidance.length > 0)
|
|
234
|
+
continue;
|
|
235
|
+
result = {
|
|
236
|
+
...turnResult,
|
|
237
|
+
tokensIn: attempts.reduce((sum, attempt) => sum + (attempt.tokensIn ?? 0), 0),
|
|
238
|
+
tokensOut: attempts.reduce((sum, attempt) => sum + (attempt.tokensOut ?? 0), 0),
|
|
239
|
+
costUsd: attempts.reduce((sum, attempt) => sum + (attempt.costUsd ?? 0), 0),
|
|
240
|
+
};
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
132
243
|
const mapping = (0, exit_conditions_cjs_1.mapExitCondition)({ ...result, killedByLeaseLoss: leaseLost || result.killedByLeaseLoss });
|
|
244
|
+
try {
|
|
245
|
+
const prUrl = await (0, github_pr_cjs_1.findHeadBranchPullRequestUrl)({
|
|
246
|
+
repository: bundle.repository?.full_name ?? task.repo,
|
|
247
|
+
headBranch: gitWorktree.branch,
|
|
248
|
+
githubToken,
|
|
249
|
+
});
|
|
250
|
+
if (prUrl)
|
|
251
|
+
mapping.evidenceUrls.push(prUrl);
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
// Evidence discovery is best-effort: a GitHub outage or token scope
|
|
255
|
+
// mismatch must not turn an otherwise valid completion into a crash.
|
|
256
|
+
log.warn(`head-branch PR lookup failed for ${leaseId}: ${String(err)}`);
|
|
257
|
+
}
|
|
133
258
|
const knownSecrets = registry.list();
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
259
|
+
if (mapping.leaseOutcome === "failed") {
|
|
260
|
+
log.warn(`adapter failed for ${leaseId}: ${(0, redact_cjs_1.redactText)(mapping.reportSummary, knownSecrets)}`);
|
|
261
|
+
}
|
|
262
|
+
const transcript = attempts
|
|
263
|
+
.flatMap((attempt, index) => [
|
|
264
|
+
`# agent turn ${index + 1} stdout`,
|
|
265
|
+
(0, redact_cjs_1.redactText)(attempt.stdout, knownSecrets),
|
|
137
266
|
"",
|
|
138
|
-
|
|
139
|
-
(0, redact_cjs_1.redactText)(
|
|
267
|
+
`# agent turn ${index + 1} stderr`,
|
|
268
|
+
(0, redact_cjs_1.redactText)(attempt.stderr, knownSecrets),
|
|
140
269
|
"",
|
|
141
|
-
]
|
|
270
|
+
])
|
|
271
|
+
.join("\n");
|
|
142
272
|
let transcriptUrl;
|
|
143
273
|
try {
|
|
144
274
|
const { upload_url, public_url } = await api.getTranscriptUploadUrl(leaseId);
|
|
@@ -160,26 +290,32 @@ async function runSession(deps, claimed, sessionId) {
|
|
|
160
290
|
transcript_url: transcriptUrl,
|
|
161
291
|
exit_status: mapping.exitStatus,
|
|
162
292
|
agent_type: config.agentType,
|
|
293
|
+
...executionReport,
|
|
163
294
|
});
|
|
164
295
|
}
|
|
165
296
|
catch (err) {
|
|
166
297
|
log.error(`session ${leaseId} threw before completing: ${String(err)}`);
|
|
298
|
+
const failureSummary = (0, redact_cjs_1.redactText)(`Session crashed: ${String(err)}`, registry.list());
|
|
167
299
|
await api
|
|
168
300
|
.completeLease(leaseId, {
|
|
169
301
|
status: "failed",
|
|
170
|
-
report:
|
|
302
|
+
report: failureSummary,
|
|
303
|
+
failure_summary: failureSummary,
|
|
171
304
|
evidence_urls: [],
|
|
172
305
|
cost: { tokens_in: 0, tokens_out: 0, cost_usd: 0 },
|
|
173
306
|
exit_status: "crashed",
|
|
174
307
|
agent_type: config.agentType,
|
|
308
|
+
...executionReport,
|
|
175
309
|
})
|
|
176
310
|
.catch((completeErr) => log.warn(`complete() after crash also failed: ${String(completeErr)}`));
|
|
177
311
|
}
|
|
178
312
|
finally {
|
|
179
313
|
clearInterval(heartbeatTimer);
|
|
314
|
+
activeAbortController?.abort();
|
|
180
315
|
secrets = {};
|
|
181
316
|
if (sandbox)
|
|
182
|
-
await sandbox.
|
|
317
|
+
await sandbox.stop();
|
|
318
|
+
await gitWorktree.cleanup();
|
|
183
319
|
await node_fs_1.promises.rm(workDir, { recursive: true, force: true }).catch(() => undefined);
|
|
184
320
|
}
|
|
185
321
|
}
|
|
@@ -191,6 +327,14 @@ function toEnvMap(secrets) {
|
|
|
191
327
|
}
|
|
192
328
|
if (secrets["github-pat"] && !out.GITHUB_TOKEN) {
|
|
193
329
|
out.GITHUB_TOKEN = secrets["github-pat"];
|
|
330
|
+
// Make ordinary `git push` calls from the agent use the in-memory token.
|
|
331
|
+
// The helper contains only an env-var reference; the token itself never
|
|
332
|
+
// lands in argv, git config, or the worktree.
|
|
333
|
+
out.GIT_CONFIG_COUNT = "1";
|
|
334
|
+
out.GIT_CONFIG_KEY_0 = "credential.helper";
|
|
335
|
+
out.GIT_CONFIG_VALUE_0 =
|
|
336
|
+
'!f() { echo username=x-access-token; echo "password=$GITHUB_TOKEN"; }; f';
|
|
337
|
+
out.GIT_TERMINAL_PROMPT = "0";
|
|
194
338
|
}
|
|
195
339
|
return out;
|
|
196
340
|
}
|
package/dist/types.cjs
CHANGED
|
@@ -6,14 +6,7 @@
|
|
|
6
6
|
// - docs/navarch/implementation-plan.md (WP-07 behavioral contract)
|
|
7
7
|
// - docs/agent-platform-project-plan.md (§3.8 dispatch/§3.9 adapter contract)
|
|
8
8
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// and were inferred from the closest analogous shape; see the WP-07 report
|
|
13
|
-
// for the full list of assumptions to confirm once those WPs land. Everything
|
|
14
|
-
// else is quoted close to verbatim from schema-design.md.
|
|
15
|
-
//
|
|
16
|
-
// runtime/src/api.cts is the ONLY place that turns these types into HTTP
|
|
17
|
-
// calls, so reconciling an assumption against the real contract is a
|
|
18
|
-
// same-file edit, not a rewrite.
|
|
9
|
+
// These contracts are implemented by the matching app/api routes and are
|
|
10
|
+
// documented in schema-design.md §7. runtime/src/api.cts is the only place
|
|
11
|
+
// that turns them into HTTP calls.
|
|
19
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sagentlab/navarch-runtime",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "Navarch machine-side session manager: registers a machine, claims tasks from the control-plane dispatcher, runs them via the Claude Code or Codex adapter, and reports results back.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"test": "vitest run",
|
|
38
38
|
"test:watch": "vitest",
|
|
39
39
|
"prepublishOnly": "npm run build && npm test",
|
|
40
|
-
"start": "node
|
|
41
|
-
"register": "node
|
|
42
|
-
"connect": "node
|
|
43
|
-
"doctor": "node
|
|
40
|
+
"start": "node bin/navarch.cjs start",
|
|
41
|
+
"register": "node bin/navarch.cjs register",
|
|
42
|
+
"connect": "node bin/navarch.cjs connect",
|
|
43
|
+
"doctor": "node bin/navarch.cjs doctor"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@types/node": "^20.14.0",
|