@nurix/nustack 0.10.0-dev.18 → 0.10.0-dev.19
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/commands/logscope.js +55 -0
- package/dist/commands/review.js +13 -4
- package/dist/commands/sessions.js +18 -3
- package/dist/index.js +27 -7
- package/dist/lib/logs/answer.js +99 -0
- package/dist/lib/logs/library.js +19 -0
- package/dist/lib/logs/lifecycle.js +159 -0
- package/dist/lib/{lanes → session-engine}/claudeAdapter.js +3 -3
- package/dist/lib/{lanes → session-engine}/frames.js +27 -27
- package/dist/lib/{lanes → session-engine}/supervisor.js +74 -73
- package/dist/lib/{lanes → session-engine}/worktree.js +7 -7
- package/dist/lib/sessions/outsideSessions.js +3 -3
- package/package.json +5 -4
- package/dist/commands/lane.js +0 -12
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { answerLogscope, listLogscopeFunctions } from "../lib/logs/answer.js";
|
|
2
|
+
import { loadLogscope } from "../lib/logs/library.js";
|
|
3
|
+
import { allowSources, forgetCorpusCommand, indexLogs, listAllowed, listCorpora, pruneCorpora, revokeSources, } from "../lib/logs/lifecycle.js";
|
|
4
|
+
export function logscopeArgv(rawArgv) {
|
|
5
|
+
const args = rawArgv.slice(2);
|
|
6
|
+
const at = args.indexOf("logscope");
|
|
7
|
+
return at === -1 ? [] : args.slice(at + 1);
|
|
8
|
+
}
|
|
9
|
+
const VALUED = ["--root", "--corpus", "--budget", "--retain", "--since", "--until", "--revision", "--note"];
|
|
10
|
+
const BARE = ["--json", "--examples", "--no-redact", "--dry-run"];
|
|
11
|
+
export function functionArgv(argv, fn) {
|
|
12
|
+
const valued = fn !== undefined && fn.args["limit"] !== undefined ? VALUED : [...VALUED, "--limit"];
|
|
13
|
+
const out = [];
|
|
14
|
+
for (let i = 0; i < argv.length; i++) {
|
|
15
|
+
const token = argv[i] ?? "";
|
|
16
|
+
if (valued.includes(token)) {
|
|
17
|
+
i++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (valued.some((flag) => token.startsWith(`${flag}=`)))
|
|
21
|
+
continue;
|
|
22
|
+
if (BARE.includes(token))
|
|
23
|
+
continue;
|
|
24
|
+
out.push(token);
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
28
|
+
function positionals(argv) {
|
|
29
|
+
return functionArgv(argv)
|
|
30
|
+
.slice(1)
|
|
31
|
+
.filter((token) => !token.startsWith("--"));
|
|
32
|
+
}
|
|
33
|
+
export async function runLogscope(rawArgv, options, cwd = process.cwd()) {
|
|
34
|
+
const argv = logscopeArgv(rawArgv);
|
|
35
|
+
const fn = argv.find((token) => !token.startsWith("--"));
|
|
36
|
+
if (fn === undefined || fn === "functions")
|
|
37
|
+
return listLogscopeFunctions(options);
|
|
38
|
+
if (fn === "index")
|
|
39
|
+
return indexLogs(positionals(argv), options, cwd);
|
|
40
|
+
if (fn === "corpora")
|
|
41
|
+
return listCorpora(options);
|
|
42
|
+
if (fn === "allow")
|
|
43
|
+
return allowSources(positionals(argv), options, cwd);
|
|
44
|
+
if (fn === "allowed")
|
|
45
|
+
return listAllowed(options, cwd);
|
|
46
|
+
if (fn === "revoke")
|
|
47
|
+
return revokeSources(positionals(argv), options, cwd);
|
|
48
|
+
if (fn === "prune")
|
|
49
|
+
return pruneCorpora(options, cwd);
|
|
50
|
+
if (fn === "forget")
|
|
51
|
+
return forgetCorpusCommand(positionals(argv)[0], options);
|
|
52
|
+
const library = await loadLogscope();
|
|
53
|
+
const rest = functionArgv(argv, library.findFn(fn));
|
|
54
|
+
return answerLogscope(fn, rest.slice(1), options, cwd);
|
|
55
|
+
}
|
package/dist/commands/review.js
CHANGED
|
@@ -77,7 +77,16 @@ function readSubject(raw) {
|
|
|
77
77
|
type: "tool_permission",
|
|
78
78
|
toolName,
|
|
79
79
|
displayName,
|
|
80
|
-
|
|
80
|
+
...(typeof row.target === "string" || row.target === null ? { target: row.target } : {}),
|
|
81
|
+
...(typeof row.inputBytes === "number" && Number.isFinite(row.inputBytes)
|
|
82
|
+
? { inputBytes: row.inputBytes }
|
|
83
|
+
: {}),
|
|
84
|
+
...(Array.isArray(row.inputFields) && row.inputFields.every((field) => typeof field === "string")
|
|
85
|
+
? { inputFields: row.inputFields }
|
|
86
|
+
: {}),
|
|
87
|
+
...(typeof row.redactionVersion === "number" && Number.isFinite(row.redactionVersion)
|
|
88
|
+
? { redactionVersion: row.redactionVersion }
|
|
89
|
+
: {}),
|
|
81
90
|
...(typeof row.description === "string" ? { description: row.description } : {}),
|
|
82
91
|
};
|
|
83
92
|
}
|
|
@@ -140,9 +149,9 @@ export function registerReviewCommands(reviewCommand, getGlobalOptions) {
|
|
|
140
149
|
.action(async (options) => runReviewCheck(options, getGlobalOptions()));
|
|
141
150
|
reviewCommand
|
|
142
151
|
.command("mirror")
|
|
143
|
-
.description("Reflect a
|
|
144
|
-
.option("--session <
|
|
145
|
-
.option("--project <id>", "the Project the
|
|
152
|
+
.description("Reflect a session's permission prompt, or its answer, into the Review pane")
|
|
153
|
+
.option("--session <sessionId>", "the session's id")
|
|
154
|
+
.option("--project <id>", "the Project the session's folder is bound to (required with --state open)")
|
|
146
155
|
.option("--state <state>", "open or answered")
|
|
147
156
|
.option("--subject <json>", "the tool-permission subject, as JSON (required with --state open)")
|
|
148
157
|
.option("--verdict <verdict>", "allow or deny (required with --state answered)")
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
import { readFile } from "node:fs/promises";
|
|
2
2
|
import { ensureAccessToken } from "../lib/auth_grant.js";
|
|
3
3
|
import { resolveNuStackUrl } from "../lib/nustack_client.js";
|
|
4
|
-
import { makeSessionsClient } from "../lib/sessions/client.js";
|
|
4
|
+
import { makeSessionsClient, sessionsClientFromEnv } from "../lib/sessions/client.js";
|
|
5
5
|
import { recordOutsideSessions, } from "../lib/sessions/outsideSessions.js";
|
|
6
|
+
import { SessionSupervisor } from "../lib/session-engine/supervisor.js";
|
|
6
7
|
import { printJsonSuccess } from "../lib/json_output.js";
|
|
8
|
+
export async function runSessionsServe(engineVersion) {
|
|
9
|
+
const supervisor = new SessionSupervisor({
|
|
10
|
+
input: process.stdin,
|
|
11
|
+
output: process.stdout,
|
|
12
|
+
engineVersion,
|
|
13
|
+
sessions: sessionsClientFromEnv(process.env),
|
|
14
|
+
});
|
|
15
|
+
await supervisor.run();
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
7
18
|
async function readScanArgument(raw) {
|
|
8
19
|
const body = raw.startsWith("@") ? await readFile(raw.slice(1), "utf8") : raw;
|
|
9
20
|
return JSON.parse(body);
|
|
@@ -46,6 +57,10 @@ export async function runSessionsRecord(options, globals) {
|
|
|
46
57
|
if (!options.scan)
|
|
47
58
|
return report("sessions record", EMPTY, json, "no scan payload");
|
|
48
59
|
const payload = (await readScanArgument(options.scan).catch(() => null));
|
|
60
|
+
if (payload && "laneSessionIds" in payload) {
|
|
61
|
+
process.stderr.write("sessions record: this NuStack desktop is older than this CLI: it sent laneSessionIds, which is now engineSessionIds. Update both.\n");
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
49
64
|
const transcripts = readTranscripts(payload?.sessions);
|
|
50
65
|
if (transcripts.length === 0)
|
|
51
66
|
return report("sessions record", EMPTY, json, "nothing to record");
|
|
@@ -54,8 +69,8 @@ export async function runSessionsRecord(options, globals) {
|
|
|
54
69
|
return report("sessions record", EMPTY, json, "signed out");
|
|
55
70
|
const summary = await recordOutsideSessions(makeSessionsClient(baseUrl, deviceAuth.accessToken), {
|
|
56
71
|
transcripts,
|
|
57
|
-
|
|
58
|
-
? payload.
|
|
72
|
+
engineSessionIds: Array.isArray(payload?.engineSessionIds)
|
|
73
|
+
? payload.engineSessionIds.filter((id) => typeof id === "string")
|
|
59
74
|
: [],
|
|
60
75
|
});
|
|
61
76
|
report("sessions record", summary, json);
|
package/dist/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import { runSync } from "./commands/sync.js";
|
|
|
9
9
|
import { runDescribe } from "./commands/describe.js";
|
|
10
10
|
import { runValidate } from "./commands/validate.js";
|
|
11
11
|
import { runGraph } from "./commands/graph.js";
|
|
12
|
+
import { runLogscope } from "./commands/logscope.js";
|
|
12
13
|
import { runDoctor } from "./commands/doctor.js";
|
|
13
14
|
import { runTokenCreate, runTokenList, runTokenRevoke } from "./commands/token.js";
|
|
14
15
|
import { runOpen } from "./commands/open.js";
|
|
@@ -23,8 +24,7 @@ import { runOrganizationList, runOrganizationCreate, runOrganizationUse } from "
|
|
|
23
24
|
import { runDocsList, runDocsShow, runDocsStatus } from "./commands/docs.js";
|
|
24
25
|
import { runHandoffList, runHandoffShow } from "./commands/handoff.js";
|
|
25
26
|
import { runToolkitStatus, runToolkitReconcile } from "./commands/toolkit.js";
|
|
26
|
-
import {
|
|
27
|
-
import { runSessionsRecord } from "./commands/sessions.js";
|
|
27
|
+
import { runSessionsRecord, runSessionsServe } from "./commands/sessions.js";
|
|
28
28
|
import { registerReviewCommands } from "./commands/review.js";
|
|
29
29
|
import { runAgentContext, runAgentHookInstall, runAgentHookRemove } from "./commands/agent.js";
|
|
30
30
|
import { printJsonError } from "./lib/json_output.js";
|
|
@@ -96,6 +96,25 @@ program
|
|
|
96
96
|
.allowUnknownOption()
|
|
97
97
|
.allowExcessArguments()
|
|
98
98
|
.action(async (_fn, _args, options) => runGraph(process.argv, options));
|
|
99
|
+
program
|
|
100
|
+
.command("logscope [fn] [args...]")
|
|
101
|
+
.description("Log tracking: ask this machine's log corpus a question, or `index` / `corpora` / `allow` / `prune`")
|
|
102
|
+
.option("--root <dir>", "the project this command is about (default: the working tree's root)")
|
|
103
|
+
.option("--corpus <ref>", "a corpus id, or a path belonging to one")
|
|
104
|
+
.option("--budget <n>", "token ceiling for the answer; 0 means unbounded")
|
|
105
|
+
.option("--limit <n>", "rows requested — beats a default budget, never an explicit one")
|
|
106
|
+
.option("--retain <days>", "retention for `index`; 0 keeps the corpus until it is forgotten")
|
|
107
|
+
.option("--examples", "keep up to three exemplar log lines per template (off by default)")
|
|
108
|
+
.option("--no-redact", "store exemplars unredacted (loud, recorded on the corpus)")
|
|
109
|
+
.option("--since <ts>", "window start for container sources")
|
|
110
|
+
.option("--until <ts>", "window end for container sources")
|
|
111
|
+
.option("--revision <sha>", "the source revision these logs were written by")
|
|
112
|
+
.option("--note <text>", "a note recorded with an `allow` grant")
|
|
113
|
+
.option("--dry-run", "for `prune`: report what would be dropped and delete nothing")
|
|
114
|
+
.option("--json", "emit the versioned machine envelope with the answer as data")
|
|
115
|
+
.allowUnknownOption()
|
|
116
|
+
.allowExcessArguments()
|
|
117
|
+
.action(async (_fn, _args, options) => runLogscope(process.argv, options));
|
|
99
118
|
program
|
|
100
119
|
.command("doctor")
|
|
101
120
|
.description("Run the full local diagnostics matrix (identity, repository, manifests, toolkit, telemetry, credentials)")
|
|
@@ -230,12 +249,13 @@ handoff
|
|
|
230
249
|
.option("--targets", "list each target's path, operation, and base semantics")
|
|
231
250
|
.option("--json", "emit the versioned machine envelope")
|
|
232
251
|
.action(async (id, options) => runHandoffShow(id, options, program.opts()));
|
|
233
|
-
const
|
|
234
|
-
|
|
252
|
+
const sessions = program
|
|
253
|
+
.command("sessions")
|
|
254
|
+
.description("Sessions on this machine — the engine the desktop drives, and the record of what ran");
|
|
255
|
+
sessions
|
|
235
256
|
.command("serve")
|
|
236
|
-
.description("Run the long-lived
|
|
237
|
-
.action(async () =>
|
|
238
|
-
const sessions = program.command("sessions").description("The session record over this machine's agent work");
|
|
257
|
+
.description("Run the long-lived session supervisor: JSONL frames on stdin/stdout (used by the NuStack desktop)")
|
|
258
|
+
.action(async () => runSessionsServe(version));
|
|
239
259
|
sessions
|
|
240
260
|
.command("record")
|
|
241
261
|
.description("Record the sessions one desktop scan found in a bound folder (used by the NuStack desktop)")
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { printJsonSuccess } from "../json_output.js";
|
|
5
|
+
import { loadLogscope } from "./library.js";
|
|
6
|
+
export class LogsNoRepository extends Error {
|
|
7
|
+
code = "LOGS_NO_REPOSITORY";
|
|
8
|
+
constructor(where) {
|
|
9
|
+
super(`not a git repository: ${where} — pass --root <path> or run inside a checkout`);
|
|
10
|
+
this.name = "LogsNoRepository";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class LogsUnavailable extends Error {
|
|
14
|
+
code = "LOGS_UNAVAILABLE";
|
|
15
|
+
constructor(reason) {
|
|
16
|
+
super(reason);
|
|
17
|
+
this.name = "LogsUnavailable";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class LogsRefused extends Error {
|
|
21
|
+
code = "LOGS_REFUSED";
|
|
22
|
+
constructor(reason) {
|
|
23
|
+
super(reason);
|
|
24
|
+
this.name = "LogsRefused";
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class LogsNotFound extends Error {
|
|
28
|
+
code = "LOGS_NOT_FOUND";
|
|
29
|
+
constructor(reason) {
|
|
30
|
+
super(reason);
|
|
31
|
+
this.name = "LogsNotFound";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function resolveRoot(root, cwd) {
|
|
35
|
+
if (root !== undefined) {
|
|
36
|
+
const path = resolve(cwd, root);
|
|
37
|
+
if (!existsSync(path))
|
|
38
|
+
throw new LogsNoRepository(root);
|
|
39
|
+
return path;
|
|
40
|
+
}
|
|
41
|
+
return gitRoot(cwd);
|
|
42
|
+
}
|
|
43
|
+
function gitRoot(from) {
|
|
44
|
+
try {
|
|
45
|
+
return execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: from, stdio: ["ignore", "pipe", "ignore"] })
|
|
46
|
+
.toString("utf8")
|
|
47
|
+
.trim();
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
throw new LogsNoRepository(from);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export const progress = (line) => {
|
|
54
|
+
process.stderr.write(`${line}\n`);
|
|
55
|
+
};
|
|
56
|
+
export async function answerLogscope(fn, argv, options, cwd) {
|
|
57
|
+
const library = await loadLogscope();
|
|
58
|
+
const known = library.findFn(fn);
|
|
59
|
+
if (known === undefined) {
|
|
60
|
+
throw new LogsUnavailable(`unknown logscope function '${fn}' — run \`nustack logscope functions\` for the list`);
|
|
61
|
+
}
|
|
62
|
+
const args = library.parseArgs(known, argv);
|
|
63
|
+
if (known.args["root"] !== undefined && args["root"] === undefined) {
|
|
64
|
+
args["root"] = resolveRoot(options.root, cwd);
|
|
65
|
+
}
|
|
66
|
+
if (args["limit"] === undefined && options.limit !== undefined)
|
|
67
|
+
args["limit"] = Number(options.limit);
|
|
68
|
+
const limit = args["limit"];
|
|
69
|
+
const budget = options.budget !== undefined ? Number(options.budget) : limit !== undefined ? 0 : library.DEFAULT_BUDGET;
|
|
70
|
+
const result = library.dispatch({ fn: known.name, args, corpus: options.corpus, budget });
|
|
71
|
+
if (result.answer.unavailable !== undefined)
|
|
72
|
+
throw new LogsUnavailable(result.answer.unavailable);
|
|
73
|
+
if (options.json === true) {
|
|
74
|
+
printJsonSuccess(`logscope ${known.name}`, result.answer);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
console.log(result.rendered);
|
|
78
|
+
}
|
|
79
|
+
export async function listLogscopeFunctions(options) {
|
|
80
|
+
const library = await loadLogscope();
|
|
81
|
+
const budget = options.budget !== undefined ? Number(options.budget) : 0;
|
|
82
|
+
const result = library.dispatch({ fn: "functions", args: {}, budget });
|
|
83
|
+
if (options.json === true) {
|
|
84
|
+
printJsonSuccess("logscope functions", result.answer);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
console.log(result.rendered);
|
|
88
|
+
console.log([
|
|
89
|
+
"",
|
|
90
|
+
"lifecycle — `nustack logscope <verb>`",
|
|
91
|
+
" allow <targets...> grant a log source; nothing is read before this",
|
|
92
|
+
" allowed the grants covering this project",
|
|
93
|
+
" revoke <targets...> withdraw grants",
|
|
94
|
+
" index <sources...> ingest or refresh a corpus from granted sources",
|
|
95
|
+
" corpora every corpus indexed on this machine",
|
|
96
|
+
" prune [--dry-run] drop every corpus past its retention",
|
|
97
|
+
" forget <id> drop one corpus and its store",
|
|
98
|
+
].join("\n"));
|
|
99
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class LogsLibraryMissing extends Error {
|
|
2
|
+
code = "LOGS_LIBRARY_MISSING";
|
|
3
|
+
constructor() {
|
|
4
|
+
super("the log-tracking library is not installed with this nustack — install it beside the CLI (`npm i -g @nurix/logscope`) or run nustack from a checkout of the monorepo.");
|
|
5
|
+
this.name = "LogsLibraryMissing";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
let cached = null;
|
|
9
|
+
export async function loadLogscope() {
|
|
10
|
+
if (cached !== null)
|
|
11
|
+
return cached;
|
|
12
|
+
try {
|
|
13
|
+
cached = (await import("@nurix/logscope"));
|
|
14
|
+
return cached;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
throw new LogsLibraryMissing();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { printJsonSuccess } from "../json_output.js";
|
|
2
|
+
import { loadLogscope } from "./library.js";
|
|
3
|
+
import { LogsNotFound, LogsRefused, progress, resolveRoot } from "./answer.js";
|
|
4
|
+
function reportPruneToStderr(library) {
|
|
5
|
+
for (const record of library.prune()) {
|
|
6
|
+
progress(`pruned ${record.id} (retention ${library.retentionOf(record)} days, indexed ${record.indexedAt ?? "unknown"})`);
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export async function indexLogs(targets, options, cwd) {
|
|
10
|
+
const library = await loadLogscope();
|
|
11
|
+
const root = resolveRoot(options.root, cwd);
|
|
12
|
+
const retentionDays = Number(options.retain ?? String(library.DEFAULT_RETENTION_DAYS));
|
|
13
|
+
if (!Number.isInteger(retentionDays) || retentionDays < 0) {
|
|
14
|
+
throw new LogsRefused("--retain takes a non-negative whole number of days");
|
|
15
|
+
}
|
|
16
|
+
reportPruneToStderr(library);
|
|
17
|
+
const outcome = await library.indexCorpus({
|
|
18
|
+
targets,
|
|
19
|
+
root,
|
|
20
|
+
since: options.since,
|
|
21
|
+
until: options.until,
|
|
22
|
+
revision: options.revision,
|
|
23
|
+
redact: options.redact !== false,
|
|
24
|
+
keepExamples: options.examples === true,
|
|
25
|
+
retentionDays,
|
|
26
|
+
});
|
|
27
|
+
if (!outcome.ok)
|
|
28
|
+
throw new LogsRefused(outcome.message);
|
|
29
|
+
if (options.json === true) {
|
|
30
|
+
printJsonSuccess("logscope index", {
|
|
31
|
+
id: outcome.id,
|
|
32
|
+
sources: outcome.sources,
|
|
33
|
+
events: outcome.events,
|
|
34
|
+
templates: outcome.templates,
|
|
35
|
+
ratio: outcome.ratio,
|
|
36
|
+
resumes: outcome.resumes,
|
|
37
|
+
retentionDays: outcome.retentionDays,
|
|
38
|
+
hasExamples: outcome.hasExamples,
|
|
39
|
+
expiresAt: outcome.expiresAt ?? null,
|
|
40
|
+
warning: outcome.warning ?? null,
|
|
41
|
+
});
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
console.log([
|
|
45
|
+
`indexed ${outcome.id}`,
|
|
46
|
+
` ${outcome.events} events -> ${outcome.templates} templates (ratio ${outcome.ratio.toFixed(4)})`,
|
|
47
|
+
` sources ${Object.entries(outcome.resumes).map(([path, resume]) => `${path} [${resume}]`).join(", ")}`,
|
|
48
|
+
` retention ${outcome.retentionDays === 0 ? "off (kept until forgotten)" : `${outcome.retentionDays} days (expires ${outcome.expiresAt ?? "unknown"})`}`,
|
|
49
|
+
` examples ${outcome.hasExamples ? "kept" : "not kept — drill and trace <id> cannot answer on this corpus"}`,
|
|
50
|
+
...(outcome.warning === undefined ? [] : [` WARNING: ${outcome.warning}`]),
|
|
51
|
+
].join("\n"));
|
|
52
|
+
}
|
|
53
|
+
export async function listCorpora(options) {
|
|
54
|
+
const library = await loadLogscope();
|
|
55
|
+
reportPruneToStderr(library);
|
|
56
|
+
const corpora = library.readRegistry().map((record) => ({
|
|
57
|
+
id: record.id,
|
|
58
|
+
sources: record.sources,
|
|
59
|
+
events: record.events ?? 0,
|
|
60
|
+
templates: record.templates ?? 0,
|
|
61
|
+
indexedAt: record.indexedAt ?? null,
|
|
62
|
+
retentionDays: library.retentionOf(record),
|
|
63
|
+
expiresAt: library.expiresAt(record) ?? null,
|
|
64
|
+
hasExamples: record.hasExamples === true,
|
|
65
|
+
}));
|
|
66
|
+
if (options.json === true) {
|
|
67
|
+
printJsonSuccess("logscope corpora", { corpora });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (corpora.length === 0) {
|
|
71
|
+
console.log("no corpus has been indexed on this machine — run: nustack logscope index <sources...>");
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
console.log(corpora
|
|
75
|
+
.map((corpus) => `${corpus.id} ${corpus.events} events, ${corpus.templates} templates (${corpus.sources.length} sources) ` +
|
|
76
|
+
(corpus.retentionDays === 0
|
|
77
|
+
? "retention off"
|
|
78
|
+
: `retention ${corpus.retentionDays} days, expires ${corpus.expiresAt ?? "unknown"}`))
|
|
79
|
+
.join("\n"));
|
|
80
|
+
}
|
|
81
|
+
export async function allowSources(targets, options, cwd) {
|
|
82
|
+
const library = await loadLogscope();
|
|
83
|
+
const root = resolveRoot(options.root, cwd);
|
|
84
|
+
const allowed = library.allow(root, targets, options.note).map((grant) => grant.path);
|
|
85
|
+
if (options.json === true) {
|
|
86
|
+
printJsonSuccess("logscope allow", { root, allowed });
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
console.log(allowed.map((path) => `allowed ${path}`).join("\n"));
|
|
90
|
+
}
|
|
91
|
+
export async function listAllowed(options, cwd) {
|
|
92
|
+
const library = await loadLogscope();
|
|
93
|
+
const root = resolveRoot(options.root, cwd);
|
|
94
|
+
const grants = library.allowedFor(root);
|
|
95
|
+
if (options.json === true) {
|
|
96
|
+
printJsonSuccess("logscope allowed", { root, grants });
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (grants.length === 0) {
|
|
100
|
+
console.log(`no source is allowed for ${root} — run: nustack logscope allow <path...>`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
console.log(grants
|
|
104
|
+
.map((grant) => `${grant.path} granted ${grant.grantedAt}${grant.note === undefined ? "" : ` ${grant.note}`}`)
|
|
105
|
+
.join("\n"));
|
|
106
|
+
}
|
|
107
|
+
export async function revokeSources(targets, options, cwd) {
|
|
108
|
+
const library = await loadLogscope();
|
|
109
|
+
const root = resolveRoot(options.root, cwd);
|
|
110
|
+
const revoked = library.revoke(root, targets);
|
|
111
|
+
if (revoked === 0)
|
|
112
|
+
throw new LogsNotFound(`no grant under ${root} names ${targets.join(", ")}`);
|
|
113
|
+
if (options.json === true) {
|
|
114
|
+
printJsonSuccess("logscope revoke", { root, revoked });
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.log(`revoked ${revoked} grant(s)`);
|
|
118
|
+
}
|
|
119
|
+
export async function pruneCorpora(options, cwd) {
|
|
120
|
+
const library = await loadLogscope();
|
|
121
|
+
resolveRoot(options.root, cwd);
|
|
122
|
+
const dryRun = options.dryRun === true;
|
|
123
|
+
const now = new Date();
|
|
124
|
+
const candidates = dryRun
|
|
125
|
+
? library.readRegistry().filter((record) => {
|
|
126
|
+
const at = library.expiresAt(record);
|
|
127
|
+
return at !== null && at !== undefined && new Date(at).getTime() <= now.getTime();
|
|
128
|
+
})
|
|
129
|
+
: library.prune();
|
|
130
|
+
const pruned = candidates.map((record) => ({
|
|
131
|
+
id: record.id,
|
|
132
|
+
retentionDays: library.retentionOf(record),
|
|
133
|
+
indexedAt: record.indexedAt ?? null,
|
|
134
|
+
}));
|
|
135
|
+
if (options.json === true) {
|
|
136
|
+
printJsonSuccess("logscope prune", { dryRun, pruned });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (pruned.length === 0) {
|
|
140
|
+
console.log("nothing to prune");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
console.log(pruned
|
|
144
|
+
.map((record) => `${dryRun ? "would prune" : "pruned"} ${record.id} (retention ${record.retentionDays} days, indexed ${record.indexedAt ?? "unknown"})`)
|
|
145
|
+
.join("\n"));
|
|
146
|
+
}
|
|
147
|
+
export async function forgetCorpusCommand(ref, options) {
|
|
148
|
+
const library = await loadLogscope();
|
|
149
|
+
if (ref === undefined)
|
|
150
|
+
throw new LogsNotFound("name the corpus to forget — run: nustack logscope corpora");
|
|
151
|
+
const forgot = library.forgetCorpus(ref);
|
|
152
|
+
if (forgot === undefined)
|
|
153
|
+
throw new LogsNotFound(`no corpus matches ${ref}`);
|
|
154
|
+
if (options.json === true) {
|
|
155
|
+
printJsonSuccess("logscope forget", { forgot });
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
console.log(`forgot ${forgot}`);
|
|
159
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
|
-
export function
|
|
3
|
+
export function claudeSessionArgs(permissionMode, resumeSessionId) {
|
|
4
4
|
const args = [
|
|
5
5
|
"-p",
|
|
6
6
|
"--input-format",
|
|
@@ -20,7 +20,7 @@ export function claudeLaneArgs(permissionMode, resumeSessionId) {
|
|
|
20
20
|
}
|
|
21
21
|
const DENY_MESSAGE = "Denied by the user in NuStack desktop.";
|
|
22
22
|
const STOP_KILL_GRACE_MS = 3_000;
|
|
23
|
-
export class
|
|
23
|
+
export class ClaudeSession {
|
|
24
24
|
child;
|
|
25
25
|
pendingApprovals = new Map();
|
|
26
26
|
interruptSeq = 0;
|
|
@@ -30,7 +30,7 @@ export class ClaudeLane {
|
|
|
30
30
|
constructor(options) {
|
|
31
31
|
this.options = options;
|
|
32
32
|
const spawnImpl = options.spawnImpl ?? spawn;
|
|
33
|
-
this.child = spawnImpl(options.claudeBin ?? "claude",
|
|
33
|
+
this.child = spawnImpl(options.claudeBin ?? "claude", claudeSessionArgs(options.permissionMode, options.resumeSessionId), {
|
|
34
34
|
cwd: options.cwd,
|
|
35
35
|
env: options.env ?? process.env,
|
|
36
36
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export const FRAME_VERSION =
|
|
1
|
+
export const FRAME_VERSION = 2;
|
|
2
2
|
const INBOUND_TYPES = new Set([
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
3
|
+
"session.open",
|
|
4
|
+
"session.turn",
|
|
5
|
+
"session.approval",
|
|
6
|
+
"session.interrupt",
|
|
7
|
+
"session.close",
|
|
8
8
|
"engine.shutdown",
|
|
9
9
|
]);
|
|
10
10
|
export function parseInboundFrame(line) {
|
|
@@ -32,37 +32,37 @@ export function parseInboundFrame(line) {
|
|
|
32
32
|
switch (type) {
|
|
33
33
|
case "engine.shutdown":
|
|
34
34
|
return { ok: true, frame: { v: FRAME_VERSION, type } };
|
|
35
|
-
case "
|
|
36
|
-
const
|
|
35
|
+
case "session.open": {
|
|
36
|
+
const sessionId = requireString("sessionId");
|
|
37
37
|
const projectPath = requireString("projectPath");
|
|
38
|
-
if (!
|
|
39
|
-
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "
|
|
38
|
+
if (!sessionId || !projectPath)
|
|
39
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "session.open requires sessionId and projectPath" } };
|
|
40
40
|
const permissionMode = typeof frame.permissionMode === "string" ? frame.permissionMode : undefined;
|
|
41
41
|
const isolated = frame.isolated === true ? true : undefined;
|
|
42
42
|
const resumeSessionId = typeof frame.resumeSessionId === "string" && frame.resumeSessionId !== "" ? frame.resumeSessionId : undefined;
|
|
43
|
-
return { ok: true, frame: { v: FRAME_VERSION, type,
|
|
43
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, sessionId, projectPath, permissionMode, isolated, resumeSessionId } };
|
|
44
44
|
}
|
|
45
|
-
case "
|
|
46
|
-
const
|
|
45
|
+
case "session.turn": {
|
|
46
|
+
const sessionId = requireString("sessionId");
|
|
47
47
|
const text = typeof frame.text === "string" ? frame.text : null;
|
|
48
|
-
if (!
|
|
49
|
-
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "
|
|
50
|
-
return { ok: true, frame: { v: FRAME_VERSION, type,
|
|
48
|
+
if (!sessionId || text === null)
|
|
49
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "session.turn requires sessionId and text" } };
|
|
50
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, sessionId, text } };
|
|
51
51
|
}
|
|
52
|
-
case "
|
|
53
|
-
const
|
|
52
|
+
case "session.approval": {
|
|
53
|
+
const sessionId = requireString("sessionId");
|
|
54
54
|
const requestId = requireString("requestId");
|
|
55
55
|
const verdict = frame.verdict === "allow" || frame.verdict === "deny" ? frame.verdict : null;
|
|
56
|
-
if (!
|
|
57
|
-
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "
|
|
58
|
-
return { ok: true, frame: { v: FRAME_VERSION, type,
|
|
56
|
+
if (!sessionId || !requestId || !verdict)
|
|
57
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: "session.approval requires sessionId, requestId, verdict" } };
|
|
58
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, sessionId, requestId, verdict } };
|
|
59
59
|
}
|
|
60
|
-
case "
|
|
61
|
-
case "
|
|
62
|
-
const
|
|
63
|
-
if (!
|
|
64
|
-
return { ok: false, error: { code: "BAD_FRAME", name: type, message: `${type} requires
|
|
65
|
-
return { ok: true, frame: { v: FRAME_VERSION, type,
|
|
60
|
+
case "session.interrupt":
|
|
61
|
+
case "session.close": {
|
|
62
|
+
const sessionId = requireString("sessionId");
|
|
63
|
+
if (!sessionId)
|
|
64
|
+
return { ok: false, error: { code: "BAD_FRAME", name: type, message: `${type} requires sessionId` } };
|
|
65
|
+
return { ok: true, frame: { v: FRAME_VERSION, type, sessionId } };
|
|
66
66
|
}
|
|
67
67
|
default:
|
|
68
68
|
return { ok: false, error: { code: "UNKNOWN_FRAME", name: type, message: `unknown frame type: ${type}` } };
|
|
@@ -2,9 +2,9 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { createInterface } from "node:readline";
|
|
5
|
-
import {
|
|
5
|
+
import { ClaudeSession } from "./claudeAdapter.js";
|
|
6
6
|
import { FRAME_VERSION, parseInboundFrame, serializeFrame, } from "./frames.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createSessionWorktree, sessionsHome } from "./worktree.js";
|
|
8
8
|
function defaultProbeClaude(env) {
|
|
9
9
|
return new Promise((resolve) => {
|
|
10
10
|
execFile("claude", ["--version"], { env, timeout: 5_000 }, (error, stdout) => {
|
|
@@ -15,11 +15,11 @@ function defaultProbeClaude(env) {
|
|
|
15
15
|
});
|
|
16
16
|
});
|
|
17
17
|
}
|
|
18
|
-
export class
|
|
18
|
+
export class SessionSupervisor {
|
|
19
19
|
options;
|
|
20
20
|
env;
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
sessions = new Map();
|
|
22
|
+
closingSessions = new Set();
|
|
23
23
|
shuttingDown = false;
|
|
24
24
|
resolveDone = null;
|
|
25
25
|
lines = null;
|
|
@@ -31,14 +31,14 @@ export class LaneSupervisor {
|
|
|
31
31
|
emit(frame) {
|
|
32
32
|
this.options.output.write(serializeFrame(frame));
|
|
33
33
|
}
|
|
34
|
-
status(
|
|
35
|
-
this.emit({ v: FRAME_VERSION, type: "
|
|
34
|
+
status(sessionId, status, message = null) {
|
|
35
|
+
this.emit({ v: FRAME_VERSION, type: "session.status", sessionId, status, message });
|
|
36
36
|
}
|
|
37
|
-
error(
|
|
38
|
-
this.emit({ v: FRAME_VERSION, type: "
|
|
37
|
+
error(sessionId, code, message) {
|
|
38
|
+
this.emit({ v: FRAME_VERSION, type: "session.error", sessionId, code, message });
|
|
39
39
|
}
|
|
40
40
|
registryPath() {
|
|
41
|
-
return path.join(
|
|
41
|
+
return path.join(sessionsHome(this.env), "registry.json");
|
|
42
42
|
}
|
|
43
43
|
async boundProjectOf(projectPath) {
|
|
44
44
|
try {
|
|
@@ -50,12 +50,12 @@ export class LaneSupervisor {
|
|
|
50
50
|
return null;
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
|
-
async
|
|
53
|
+
async moveSessionRow(record, to) {
|
|
54
54
|
const client = this.options.sessions;
|
|
55
55
|
if (!client || !record.projectId)
|
|
56
56
|
return;
|
|
57
57
|
try {
|
|
58
|
-
await client.transitionSession(record.projectId, record.
|
|
58
|
+
await client.transitionSession(record.projectId, record.sessionId, to);
|
|
59
59
|
}
|
|
60
60
|
catch {
|
|
61
61
|
}
|
|
@@ -63,17 +63,17 @@ export class LaneSupervisor {
|
|
|
63
63
|
async updateRegistry(mutate) {
|
|
64
64
|
const run = async () => {
|
|
65
65
|
const file = this.registryPath();
|
|
66
|
-
let
|
|
66
|
+
let sessions = [];
|
|
67
67
|
try {
|
|
68
68
|
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
69
|
-
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.
|
|
70
|
-
|
|
69
|
+
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.sessions)) {
|
|
70
|
+
sessions = parsed.sessions;
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
catch {
|
|
74
74
|
}
|
|
75
75
|
await mkdir(path.dirname(file), { recursive: true });
|
|
76
|
-
await writeFile(file, JSON.stringify({ v:
|
|
76
|
+
await writeFile(file, JSON.stringify({ v: 2, sessions: mutate(sessions) }, null, 2) + "\n", "utf8");
|
|
77
77
|
};
|
|
78
78
|
this.registryWrites = this.registryWrites.then(run, run);
|
|
79
79
|
return this.registryWrites;
|
|
@@ -104,48 +104,48 @@ export class LaneSupervisor {
|
|
|
104
104
|
case "engine.shutdown":
|
|
105
105
|
await this.shutdown();
|
|
106
106
|
return;
|
|
107
|
-
case "
|
|
108
|
-
await this.
|
|
107
|
+
case "session.open":
|
|
108
|
+
await this.openSession(frame.sessionId, frame.projectPath, frame.permissionMode, frame.isolated, frame.resumeSessionId);
|
|
109
109
|
return;
|
|
110
|
-
case "
|
|
111
|
-
const
|
|
112
|
-
if (!
|
|
113
|
-
return this.error(frame.
|
|
114
|
-
|
|
115
|
-
this.status(frame.
|
|
110
|
+
case "session.turn": {
|
|
111
|
+
const session = this.sessions.get(frame.sessionId);
|
|
112
|
+
if (!session)
|
|
113
|
+
return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
|
|
114
|
+
session.adapter.sendTurn(frame.text);
|
|
115
|
+
this.status(frame.sessionId, "running");
|
|
116
116
|
return;
|
|
117
117
|
}
|
|
118
|
-
case "
|
|
119
|
-
const
|
|
120
|
-
if (!
|
|
121
|
-
return this.error(frame.
|
|
122
|
-
|
|
118
|
+
case "session.approval": {
|
|
119
|
+
const session = this.sessions.get(frame.sessionId);
|
|
120
|
+
if (!session)
|
|
121
|
+
return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
|
|
122
|
+
session.adapter.respondApproval(frame.requestId, frame.verdict);
|
|
123
123
|
return;
|
|
124
124
|
}
|
|
125
|
-
case "
|
|
126
|
-
const
|
|
127
|
-
if (!
|
|
128
|
-
return this.error(frame.
|
|
129
|
-
|
|
125
|
+
case "session.interrupt": {
|
|
126
|
+
const session = this.sessions.get(frame.sessionId);
|
|
127
|
+
if (!session)
|
|
128
|
+
return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
|
|
129
|
+
session.adapter.interrupt();
|
|
130
130
|
return;
|
|
131
131
|
}
|
|
132
|
-
case "
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
135
|
-
return this.error(frame.
|
|
136
|
-
this.
|
|
137
|
-
|
|
132
|
+
case "session.close": {
|
|
133
|
+
const session = this.sessions.get(frame.sessionId);
|
|
134
|
+
if (!session)
|
|
135
|
+
return this.error(frame.sessionId, "SESSION_UNKNOWN", `no open session ${frame.sessionId}`);
|
|
136
|
+
this.closingSessions.add(frame.sessionId);
|
|
137
|
+
session.adapter.stop();
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
|
-
async
|
|
143
|
-
if (this.
|
|
144
|
-
return this.error(
|
|
145
|
-
this.status(
|
|
146
|
-
const worktree = await
|
|
142
|
+
async openSession(sessionId, projectPath, permissionMode, isolated, resumeSessionId) {
|
|
143
|
+
if (this.sessions.has(sessionId))
|
|
144
|
+
return this.error(sessionId, "SESSION_ALREADY_OPEN", `session ${sessionId} is already open`);
|
|
145
|
+
this.status(sessionId, "starting");
|
|
146
|
+
const worktree = await createSessionWorktree(projectPath, sessionId, this.env, isolated === true);
|
|
147
147
|
const record = {
|
|
148
|
-
|
|
148
|
+
sessionId,
|
|
149
149
|
projectPath,
|
|
150
150
|
worktreePath: worktree.worktreePath,
|
|
151
151
|
branch: worktree.branch,
|
|
@@ -153,6 +153,7 @@ export class LaneSupervisor {
|
|
|
153
153
|
providerSessionId: null,
|
|
154
154
|
createdAt: new Date().toISOString(),
|
|
155
155
|
status: "open",
|
|
156
|
+
nustack: 1,
|
|
156
157
|
projectId: await this.boundProjectOf(projectPath),
|
|
157
158
|
};
|
|
158
159
|
const adapterOptions = {
|
|
@@ -161,16 +162,16 @@ export class LaneSupervisor {
|
|
|
161
162
|
resumeSessionId,
|
|
162
163
|
env: this.env,
|
|
163
164
|
onEvent: (event) => {
|
|
164
|
-
this.emit({ v: FRAME_VERSION, type: "
|
|
165
|
+
this.emit({ v: FRAME_VERSION, type: "session.event", sessionId, event });
|
|
165
166
|
if (typeof event === "object" && event !== null && event.type === "result") {
|
|
166
|
-
this.status(
|
|
167
|
+
this.status(sessionId, "ready");
|
|
167
168
|
}
|
|
168
169
|
},
|
|
169
170
|
onApprovalRequest: (request) => {
|
|
170
171
|
this.emit({
|
|
171
172
|
v: FRAME_VERSION,
|
|
172
|
-
type: "
|
|
173
|
-
|
|
173
|
+
type: "session.approvalRequest",
|
|
174
|
+
sessionId,
|
|
174
175
|
requestId: request.requestId,
|
|
175
176
|
toolName: request.toolName,
|
|
176
177
|
displayName: request.displayName,
|
|
@@ -178,43 +179,43 @@ export class LaneSupervisor {
|
|
|
178
179
|
description: request.description,
|
|
179
180
|
});
|
|
180
181
|
},
|
|
181
|
-
onProtocolError: (code, message) => this.error(
|
|
182
|
-
onSessionId: (
|
|
183
|
-
record.providerSessionId =
|
|
184
|
-
void this.updateRegistry((
|
|
182
|
+
onProtocolError: (code, message) => this.error(sessionId, code, message),
|
|
183
|
+
onSessionId: (vendorSessionId) => {
|
|
184
|
+
record.providerSessionId = vendorSessionId;
|
|
185
|
+
void this.updateRegistry((sessions) => sessions.map((row) => (row.sessionId === sessionId ? { ...row, providerSessionId: vendorSessionId } : row)));
|
|
185
186
|
const client = this.options.sessions;
|
|
186
187
|
if (client && record.projectId) {
|
|
187
|
-
void client.setVendorSessionId(record.projectId,
|
|
188
|
+
void client.setVendorSessionId(record.projectId, sessionId, vendorSessionId).catch(() => undefined);
|
|
188
189
|
}
|
|
189
190
|
},
|
|
190
191
|
onExit: (code) => {
|
|
191
|
-
const wasExpected = this.
|
|
192
|
-
this.
|
|
192
|
+
const wasExpected = this.closingSessions.delete(sessionId) || this.shuttingDown;
|
|
193
|
+
this.sessions.delete(sessionId);
|
|
193
194
|
if (!wasExpected)
|
|
194
|
-
this.error(
|
|
195
|
-
this.emit({ v: FRAME_VERSION, type: "
|
|
196
|
-
void this.updateRegistry((
|
|
197
|
-
void this.
|
|
195
|
+
this.error(sessionId, "ADAPTER_FAILED", `claude exited unexpectedly (code ${String(code)})`);
|
|
196
|
+
this.emit({ v: FRAME_VERSION, type: "session.closed", sessionId });
|
|
197
|
+
void this.updateRegistry((sessions) => sessions.map((row) => (row.sessionId === sessionId ? { ...row, status: "closed" } : row)));
|
|
198
|
+
void this.moveSessionRow(record, wasExpected ? "done" : "failed");
|
|
198
199
|
},
|
|
199
200
|
};
|
|
200
|
-
const create = this.options.createAdapter ?? ((opts) => new
|
|
201
|
+
const create = this.options.createAdapter ?? ((opts) => new ClaudeSession(opts));
|
|
201
202
|
let adapter;
|
|
202
203
|
try {
|
|
203
204
|
adapter = create(adapterOptions);
|
|
204
205
|
}
|
|
205
206
|
catch (error) {
|
|
206
|
-
return this.error(
|
|
207
|
+
return this.error(sessionId, "ADAPTER_FAILED", error instanceof Error ? error.message : String(error));
|
|
207
208
|
}
|
|
208
|
-
this.
|
|
209
|
-
await this.updateRegistry((
|
|
209
|
+
this.sessions.set(sessionId, { adapter, record });
|
|
210
|
+
await this.updateRegistry((sessions) => [...sessions.filter((row) => row.sessionId !== sessionId), record]);
|
|
210
211
|
if (this.options.sessions && record.projectId) {
|
|
211
212
|
try {
|
|
212
213
|
await this.options.sessions.createSession(record.projectId, {
|
|
213
|
-
id:
|
|
214
|
+
id: sessionId,
|
|
214
215
|
startedBy: "person",
|
|
215
216
|
substrate: "device",
|
|
216
217
|
mode: "synchronous",
|
|
217
|
-
|
|
218
|
+
nustack: 1,
|
|
218
219
|
vendor: "claude_code",
|
|
219
220
|
renderer: "claude_transcript",
|
|
220
221
|
status: "created",
|
|
@@ -225,22 +226,22 @@ export class LaneSupervisor {
|
|
|
225
226
|
}
|
|
226
227
|
this.emit({
|
|
227
228
|
v: FRAME_VERSION,
|
|
228
|
-
type: "
|
|
229
|
-
|
|
229
|
+
type: "session.opened",
|
|
230
|
+
sessionId,
|
|
230
231
|
projectPath,
|
|
231
232
|
worktreePath: worktree.worktreePath,
|
|
232
233
|
branch: worktree.branch,
|
|
233
234
|
isolated: worktree.isolated,
|
|
234
235
|
});
|
|
235
|
-
this.status(
|
|
236
|
+
this.status(sessionId, "ready");
|
|
236
237
|
}
|
|
237
238
|
async shutdown() {
|
|
238
239
|
if (this.shuttingDown)
|
|
239
240
|
return;
|
|
240
241
|
this.shuttingDown = true;
|
|
241
|
-
for (const [,
|
|
242
|
-
|
|
243
|
-
this.
|
|
242
|
+
for (const [, session] of this.sessions)
|
|
243
|
+
session.adapter.stop();
|
|
244
|
+
this.sessions.clear();
|
|
244
245
|
this.lines?.close();
|
|
245
246
|
this.resolveDone?.();
|
|
246
247
|
}
|
|
@@ -2,12 +2,12 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { mkdir } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
export const
|
|
6
|
-
export function
|
|
7
|
-
const override = env.
|
|
5
|
+
export const ENGINE_SESSION_BRANCH_PREFIX = "nustack/session-";
|
|
6
|
+
export function sessionsHome(env = process.env) {
|
|
7
|
+
const override = env.NUSTACK_SESSIONS_HOME;
|
|
8
8
|
if (override && override !== "")
|
|
9
9
|
return override;
|
|
10
|
-
return path.join(os.homedir(), ".nustack", "
|
|
10
|
+
return path.join(os.homedir(), ".nustack", "sessions");
|
|
11
11
|
}
|
|
12
12
|
function git(args, cwd) {
|
|
13
13
|
return new Promise((resolve) => {
|
|
@@ -16,15 +16,15 @@ function git(args, cwd) {
|
|
|
16
16
|
});
|
|
17
17
|
});
|
|
18
18
|
}
|
|
19
|
-
export async function
|
|
19
|
+
export async function createSessionWorktree(projectPath, sessionId, env = process.env, isolate = false) {
|
|
20
20
|
const inPlace = { worktreePath: projectPath, branch: null, isolated: false };
|
|
21
21
|
if (!isolate)
|
|
22
22
|
return inPlace;
|
|
23
23
|
const isRepo = await git(["rev-parse", "--is-inside-work-tree"], projectPath);
|
|
24
24
|
if (isRepo !== "true")
|
|
25
25
|
return inPlace;
|
|
26
|
-
const branch = `${
|
|
27
|
-
const worktreePath = path.join(
|
|
26
|
+
const branch = `${ENGINE_SESSION_BRANCH_PREFIX}${sessionId}`;
|
|
27
|
+
const worktreePath = path.join(sessionsHome(env), "worktrees", sessionId);
|
|
28
28
|
await mkdir(path.dirname(worktreePath), { recursive: true });
|
|
29
29
|
const added = await git(["worktree", "add", worktreePath, "-b", branch], projectPath);
|
|
30
30
|
if (added === null)
|
|
@@ -9,7 +9,7 @@ function vendorOf(provider) {
|
|
|
9
9
|
const AT_REST = new Set(["done", "failed", "cancelled"]);
|
|
10
10
|
export async function recordOutsideSessions(client, input) {
|
|
11
11
|
const now = input.now ?? Date.now;
|
|
12
|
-
const
|
|
12
|
+
const engineIds = new Set((input.engineSessionIds ?? []).filter((id) => Boolean(id)));
|
|
13
13
|
const report = { created: 0, finished: 0, skipped: 0, failed: 0 };
|
|
14
14
|
for (const transcript of input.transcripts) {
|
|
15
15
|
if (!transcript.projectId) {
|
|
@@ -23,14 +23,14 @@ export async function recordOutsideSessions(client, input) {
|
|
|
23
23
|
}
|
|
24
24
|
const stale = now() - transcript.modifiedMs >= STOPPED_GROWING_MS;
|
|
25
25
|
const vendorSessionId = transcript.vendorSessionId?.trim() || null;
|
|
26
|
-
const
|
|
26
|
+
const isEngineSession = vendorSessionId !== null && engineIds.has(vendorSessionId);
|
|
27
27
|
try {
|
|
28
28
|
const { created, session } = await client.createSession(transcript.projectId, {
|
|
29
29
|
id: transcript.sessionKey,
|
|
30
30
|
startedBy: "person",
|
|
31
31
|
substrate: "device",
|
|
32
32
|
mode: "synchronous",
|
|
33
|
-
|
|
33
|
+
nustack: isEngineSession ? 1 : 0,
|
|
34
34
|
vendor: kinds.vendor,
|
|
35
35
|
renderer: kinds.renderer,
|
|
36
36
|
status: stale ? "done" : "running",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nurix/nustack",
|
|
3
|
-
"version": "0.10.0-dev.
|
|
3
|
+
"version": "0.10.0-dev.19",
|
|
4
4
|
"description": "The nustack CLI — bootstrap NuStack services into a repo via the NuStack discovery plane.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -26,15 +26,16 @@
|
|
|
26
26
|
"commander": "^14.0.0"
|
|
27
27
|
},
|
|
28
28
|
"optionalDependencies": {
|
|
29
|
-
"@nurix/codegraph": "0.2.1"
|
|
29
|
+
"@nurix/codegraph": "0.2.1",
|
|
30
|
+
"@nurix/logscope": "0.1.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
33
|
"@ngneat/falso": "^8.0.2",
|
|
33
34
|
"@types/node": "^22.19.20",
|
|
34
35
|
"typescript": "^5.7.2",
|
|
35
36
|
"vitest": "^4.1.11",
|
|
36
|
-
"@nurix/nustack.
|
|
37
|
-
"@nurix/nustack.
|
|
37
|
+
"@nurix/nustack.ui": "0.0.0",
|
|
38
|
+
"@nurix/nustack.studio": "0.0.0"
|
|
38
39
|
},
|
|
39
40
|
"publishConfig": {
|
|
40
41
|
"access": "public"
|
package/dist/commands/lane.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { LaneSupervisor } from "../lib/lanes/supervisor.js";
|
|
2
|
-
import { sessionsClientFromEnv } from "../lib/sessions/client.js";
|
|
3
|
-
export async function runLaneServe(engineVersion) {
|
|
4
|
-
const supervisor = new LaneSupervisor({
|
|
5
|
-
input: process.stdin,
|
|
6
|
-
output: process.stdout,
|
|
7
|
-
engineVersion,
|
|
8
|
-
sessions: sessionsClientFromEnv(process.env),
|
|
9
|
-
});
|
|
10
|
-
await supervisor.run();
|
|
11
|
-
process.exit(0);
|
|
12
|
-
}
|