@nurix/nustack 0.10.0-dev.8 → 0.11.0
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 +1 -0
- package/dist/commands/bootstrap.js +4 -4
- package/dist/commands/docs.js +13 -13
- package/dist/commands/doctor.js +51 -17
- package/dist/commands/handoff.js +8 -8
- package/dist/commands/init.js +43 -43
- package/dist/commands/login.js +19 -6
- package/dist/commands/logout.js +14 -6
- package/dist/commands/logscope.js +55 -0
- package/dist/commands/{workspace-sync.js → project-sync.js} +5 -5
- package/dist/commands/{workspace.js → project.js} +133 -133
- package/dist/commands/review.js +38 -15
- package/dist/commands/search.js +7 -7
- package/dist/commands/sessions.js +156 -5
- package/dist/commands/sync.js +10 -10
- package/dist/commands/token.js +102 -0
- package/dist/commands/toolkit.js +19 -19
- package/dist/index.js +88 -36
- package/dist/lib/auth_grant.js +32 -0
- package/dist/lib/auth_recovery.js +3 -1
- package/dist/lib/credential_store.js +106 -0
- package/dist/lib/device_identity.js +6 -6
- package/dist/lib/device_keychain.js +16 -37
- package/dist/lib/etna/profile.js +9 -9
- package/dist/lib/etna/reconcile.js +11 -11
- package/dist/lib/git-sync/core/engine.js +4 -4
- package/dist/lib/git-sync/inbound/apply_commit.js +3 -3
- package/dist/lib/git-sync/inbound/approval.js +1 -1
- package/dist/lib/git-sync/inbound/download.js +8 -8
- package/dist/lib/git-sync/inbound/event_sync.js +13 -13
- package/dist/lib/git-sync/inbound/notification.js +1 -1
- package/dist/lib/git-sync/outbound/extraction.js +4 -4
- package/dist/lib/git-sync/outbound/history_decision.js +2 -2
- package/dist/lib/git-sync/outbound/publisher.js +2 -2
- package/dist/lib/git-sync/outbound/queue.js +3 -3
- package/dist/lib/git-sync/outbound/upload_client.js +4 -4
- package/dist/lib/git-sync/whitelist/index.js +1 -1
- package/dist/lib/git-sync/whitelist/types.js +1 -1
- package/dist/lib/git-sync/whitelist/validation.js +5 -5
- package/dist/lib/libsecret_store.js +47 -0
- package/dist/lib/local_store/accepted_uploads.js +5 -5
- package/dist/lib/local_store/bindings.js +11 -11
- package/dist/lib/local_store/index.js +1 -1
- package/dist/lib/local_store/migrations.js +13 -0
- package/dist/lib/local_store/project_events_cache.js +54 -0
- 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/nustack_client.js +88 -46
- package/dist/lib/onboarding/attach_existing_git.js +7 -7
- package/dist/lib/onboarding/binding.js +11 -11
- package/dist/lib/onboarding/folder_state.js +10 -10
- package/dist/lib/onboarding/handoff_apply.js +2 -2
- package/dist/lib/onboarding/init_fresh_folder.js +18 -18
- package/dist/lib/onboarding/manifest_writer.js +31 -31
- package/dist/lib/onboarding/web_first_attach.js +12 -12
- package/dist/lib/{workspace_context.js → project_context.js} +24 -24
- package/dist/lib/review/client.js +1 -1
- package/dist/lib/{lanes → session-engine}/claudeAdapter.js +3 -3
- package/dist/lib/{lanes → session-engine}/frames.js +36 -27
- package/dist/lib/session-engine/prune.js +233 -0
- package/dist/lib/session-engine/registry.js +83 -0
- package/dist/lib/session-engine/supervisor.js +321 -0
- package/dist/lib/session-engine/worktree.js +89 -0
- package/dist/lib/session_telemetry.js +2 -2
- package/dist/lib/sessions/client.js +8 -8
- package/dist/lib/sessions/outsideSessions.js +6 -6
- package/dist/lib/telemetry/config_renderer.js +3 -3
- package/dist/lib/telemetry/health_report.js +1 -1
- package/package.json +5 -4
- package/dist/commands/lane.js +0 -12
- package/dist/lib/lanes/supervisor.js +0 -247
- package/dist/lib/lanes/worktree.js +0 -33
- package/dist/lib/local_store/workspace_events_cache.js +0 -54
|
@@ -1,247 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
|
-
import path from "node:path";
|
|
4
|
-
import { createInterface } from "node:readline";
|
|
5
|
-
import { ClaudeLane } from "./claudeAdapter.js";
|
|
6
|
-
import { FRAME_VERSION, parseInboundFrame, serializeFrame, } from "./frames.js";
|
|
7
|
-
import { createLaneWorktree, lanesHome } from "./worktree.js";
|
|
8
|
-
function defaultProbeClaude(env) {
|
|
9
|
-
return new Promise((resolve) => {
|
|
10
|
-
execFile("claude", ["--version"], { env, timeout: 5_000 }, (error, stdout) => {
|
|
11
|
-
if (error)
|
|
12
|
-
resolve({ found: false, version: null });
|
|
13
|
-
else
|
|
14
|
-
resolve({ found: true, version: stdout.trim() });
|
|
15
|
-
});
|
|
16
|
-
});
|
|
17
|
-
}
|
|
18
|
-
export class LaneSupervisor {
|
|
19
|
-
options;
|
|
20
|
-
env;
|
|
21
|
-
lanes = new Map();
|
|
22
|
-
closingLanes = new Set();
|
|
23
|
-
shuttingDown = false;
|
|
24
|
-
resolveDone = null;
|
|
25
|
-
lines = null;
|
|
26
|
-
registryWrites = Promise.resolve();
|
|
27
|
-
constructor(options) {
|
|
28
|
-
this.options = options;
|
|
29
|
-
this.env = options.env ?? process.env;
|
|
30
|
-
}
|
|
31
|
-
emit(frame) {
|
|
32
|
-
this.options.output.write(serializeFrame(frame));
|
|
33
|
-
}
|
|
34
|
-
status(laneId, status, message = null) {
|
|
35
|
-
this.emit({ v: FRAME_VERSION, type: "lane.status", laneId, status, message });
|
|
36
|
-
}
|
|
37
|
-
error(laneId, code, message) {
|
|
38
|
-
this.emit({ v: FRAME_VERSION, type: "lane.error", laneId, code, message });
|
|
39
|
-
}
|
|
40
|
-
registryPath() {
|
|
41
|
-
return path.join(lanesHome(this.env), "registry.json");
|
|
42
|
-
}
|
|
43
|
-
async boundWorkspaceOf(projectPath) {
|
|
44
|
-
try {
|
|
45
|
-
const parsed = JSON.parse(await readFile(path.join(projectPath, ".nustack", "workspace.json"), "utf8"));
|
|
46
|
-
const id = parsed?.workspaceId;
|
|
47
|
-
return typeof id === "string" && id.trim() ? id.trim() : null;
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
async moveLaneSession(record, to) {
|
|
54
|
-
const client = this.options.sessions;
|
|
55
|
-
if (!client || !record.workspaceId)
|
|
56
|
-
return;
|
|
57
|
-
try {
|
|
58
|
-
await client.transitionSession(record.workspaceId, record.laneId, to);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
async updateRegistry(mutate) {
|
|
64
|
-
const run = async () => {
|
|
65
|
-
const file = this.registryPath();
|
|
66
|
-
let lanes = [];
|
|
67
|
-
try {
|
|
68
|
-
const parsed = JSON.parse(await readFile(file, "utf8"));
|
|
69
|
-
if (typeof parsed === "object" && parsed !== null && Array.isArray(parsed.lanes)) {
|
|
70
|
-
lanes = parsed.lanes;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
}
|
|
75
|
-
await mkdir(path.dirname(file), { recursive: true });
|
|
76
|
-
await writeFile(file, JSON.stringify({ v: 1, lanes: mutate(lanes) }, null, 2) + "\n", "utf8");
|
|
77
|
-
};
|
|
78
|
-
this.registryWrites = this.registryWrites.then(run, run);
|
|
79
|
-
return this.registryWrites;
|
|
80
|
-
}
|
|
81
|
-
async run() {
|
|
82
|
-
const probe = this.options.probeClaude ?? (() => defaultProbeClaude(this.env));
|
|
83
|
-
const claude = await probe();
|
|
84
|
-
this.emit({ v: FRAME_VERSION, type: "engine.hello", engineVersion: this.options.engineVersion, claude });
|
|
85
|
-
const lines = createInterface({ input: this.options.input });
|
|
86
|
-
this.lines = lines;
|
|
87
|
-
lines.on("line", (line) => {
|
|
88
|
-
if (line.trim() === "")
|
|
89
|
-
return;
|
|
90
|
-
const parsed = parseInboundFrame(line);
|
|
91
|
-
if (!parsed.ok) {
|
|
92
|
-
this.error(null, parsed.error.code, parsed.error.message);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
void this.handle(parsed.frame);
|
|
96
|
-
});
|
|
97
|
-
await new Promise((resolve) => {
|
|
98
|
-
this.resolveDone = resolve;
|
|
99
|
-
lines.on("close", () => void this.shutdown());
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
async handle(frame) {
|
|
103
|
-
switch (frame.type) {
|
|
104
|
-
case "engine.shutdown":
|
|
105
|
-
await this.shutdown();
|
|
106
|
-
return;
|
|
107
|
-
case "lane.open":
|
|
108
|
-
await this.openLane(frame.laneId, frame.projectPath, frame.permissionMode, frame.isolated, frame.resumeSessionId);
|
|
109
|
-
return;
|
|
110
|
-
case "lane.turn": {
|
|
111
|
-
const lane = this.lanes.get(frame.laneId);
|
|
112
|
-
if (!lane)
|
|
113
|
-
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
114
|
-
lane.adapter.sendTurn(frame.text);
|
|
115
|
-
this.status(frame.laneId, "running");
|
|
116
|
-
return;
|
|
117
|
-
}
|
|
118
|
-
case "lane.approval": {
|
|
119
|
-
const lane = this.lanes.get(frame.laneId);
|
|
120
|
-
if (!lane)
|
|
121
|
-
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
122
|
-
lane.adapter.respondApproval(frame.requestId, frame.verdict);
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
case "lane.interrupt": {
|
|
126
|
-
const lane = this.lanes.get(frame.laneId);
|
|
127
|
-
if (!lane)
|
|
128
|
-
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
129
|
-
lane.adapter.interrupt();
|
|
130
|
-
return;
|
|
131
|
-
}
|
|
132
|
-
case "lane.close": {
|
|
133
|
-
const lane = this.lanes.get(frame.laneId);
|
|
134
|
-
if (!lane)
|
|
135
|
-
return this.error(frame.laneId, "LANE_UNKNOWN", `no open lane ${frame.laneId}`);
|
|
136
|
-
this.closingLanes.add(frame.laneId);
|
|
137
|
-
lane.adapter.stop();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async openLane(laneId, projectPath, permissionMode, isolated, resumeSessionId) {
|
|
143
|
-
if (this.lanes.has(laneId))
|
|
144
|
-
return this.error(laneId, "LANE_ALREADY_OPEN", `lane ${laneId} is already open`);
|
|
145
|
-
this.status(laneId, "starting");
|
|
146
|
-
const worktree = await createLaneWorktree(projectPath, laneId, this.env, isolated === true);
|
|
147
|
-
const record = {
|
|
148
|
-
laneId,
|
|
149
|
-
projectPath,
|
|
150
|
-
worktreePath: worktree.worktreePath,
|
|
151
|
-
branch: worktree.branch,
|
|
152
|
-
provider: "claude-code",
|
|
153
|
-
providerSessionId: null,
|
|
154
|
-
createdAt: new Date().toISOString(),
|
|
155
|
-
status: "open",
|
|
156
|
-
workspaceId: await this.boundWorkspaceOf(projectPath),
|
|
157
|
-
};
|
|
158
|
-
const adapterOptions = {
|
|
159
|
-
cwd: worktree.worktreePath,
|
|
160
|
-
permissionMode,
|
|
161
|
-
resumeSessionId,
|
|
162
|
-
env: this.env,
|
|
163
|
-
onEvent: (event) => {
|
|
164
|
-
this.emit({ v: FRAME_VERSION, type: "lane.event", laneId, event });
|
|
165
|
-
if (typeof event === "object" && event !== null && event.type === "result") {
|
|
166
|
-
this.status(laneId, "ready");
|
|
167
|
-
}
|
|
168
|
-
},
|
|
169
|
-
onApprovalRequest: (request) => {
|
|
170
|
-
this.emit({
|
|
171
|
-
v: FRAME_VERSION,
|
|
172
|
-
type: "lane.approvalRequest",
|
|
173
|
-
laneId,
|
|
174
|
-
requestId: request.requestId,
|
|
175
|
-
toolName: request.toolName,
|
|
176
|
-
displayName: request.displayName,
|
|
177
|
-
toolInput: request.toolInput,
|
|
178
|
-
description: request.description,
|
|
179
|
-
});
|
|
180
|
-
},
|
|
181
|
-
onProtocolError: (code, message) => this.error(laneId, code, message),
|
|
182
|
-
onSessionId: (sessionId) => {
|
|
183
|
-
record.providerSessionId = sessionId;
|
|
184
|
-
void this.updateRegistry((lanes) => lanes.map((row) => (row.laneId === laneId ? { ...row, providerSessionId: sessionId } : row)));
|
|
185
|
-
const client = this.options.sessions;
|
|
186
|
-
if (client && record.workspaceId) {
|
|
187
|
-
void client.setVendorSessionId(record.workspaceId, laneId, sessionId).catch(() => undefined);
|
|
188
|
-
}
|
|
189
|
-
},
|
|
190
|
-
onExit: (code) => {
|
|
191
|
-
const wasExpected = this.closingLanes.delete(laneId) || this.shuttingDown;
|
|
192
|
-
this.lanes.delete(laneId);
|
|
193
|
-
if (!wasExpected)
|
|
194
|
-
this.error(laneId, "ADAPTER_FAILED", `claude exited unexpectedly (code ${String(code)})`);
|
|
195
|
-
this.emit({ v: FRAME_VERSION, type: "lane.closed", laneId });
|
|
196
|
-
void this.updateRegistry((lanes) => lanes.map((row) => (row.laneId === laneId ? { ...row, status: "closed" } : row)));
|
|
197
|
-
void this.moveLaneSession(record, wasExpected ? "done" : "failed");
|
|
198
|
-
},
|
|
199
|
-
};
|
|
200
|
-
const create = this.options.createAdapter ?? ((opts) => new ClaudeLane(opts));
|
|
201
|
-
let adapter;
|
|
202
|
-
try {
|
|
203
|
-
adapter = create(adapterOptions);
|
|
204
|
-
}
|
|
205
|
-
catch (error) {
|
|
206
|
-
return this.error(laneId, "ADAPTER_FAILED", error instanceof Error ? error.message : String(error));
|
|
207
|
-
}
|
|
208
|
-
this.lanes.set(laneId, { adapter, record });
|
|
209
|
-
await this.updateRegistry((lanes) => [...lanes.filter((row) => row.laneId !== laneId), record]);
|
|
210
|
-
if (this.options.sessions && record.workspaceId) {
|
|
211
|
-
try {
|
|
212
|
-
await this.options.sessions.createSession(record.workspaceId, {
|
|
213
|
-
id: laneId,
|
|
214
|
-
startedBy: "person",
|
|
215
|
-
substrate: "device",
|
|
216
|
-
mode: "synchronous",
|
|
217
|
-
origin: "inside",
|
|
218
|
-
vendor: "claude_code",
|
|
219
|
-
renderer: "claude_transcript",
|
|
220
|
-
status: "created",
|
|
221
|
-
});
|
|
222
|
-
}
|
|
223
|
-
catch {
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
this.emit({
|
|
227
|
-
v: FRAME_VERSION,
|
|
228
|
-
type: "lane.opened",
|
|
229
|
-
laneId,
|
|
230
|
-
projectPath,
|
|
231
|
-
worktreePath: worktree.worktreePath,
|
|
232
|
-
branch: worktree.branch,
|
|
233
|
-
isolated: worktree.isolated,
|
|
234
|
-
});
|
|
235
|
-
this.status(laneId, "ready");
|
|
236
|
-
}
|
|
237
|
-
async shutdown() {
|
|
238
|
-
if (this.shuttingDown)
|
|
239
|
-
return;
|
|
240
|
-
this.shuttingDown = true;
|
|
241
|
-
for (const [, lane] of this.lanes)
|
|
242
|
-
lane.adapter.stop();
|
|
243
|
-
this.lanes.clear();
|
|
244
|
-
this.lines?.close();
|
|
245
|
-
this.resolveDone?.();
|
|
246
|
-
}
|
|
247
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import os from "node:os";
|
|
4
|
-
import path from "node:path";
|
|
5
|
-
export const LANE_BRANCH_PREFIX = "nustack/lane-";
|
|
6
|
-
export function lanesHome(env = process.env) {
|
|
7
|
-
const override = env.NUSTACK_LANES_HOME;
|
|
8
|
-
if (override && override !== "")
|
|
9
|
-
return override;
|
|
10
|
-
return path.join(os.homedir(), ".nustack", "lanes");
|
|
11
|
-
}
|
|
12
|
-
function git(args, cwd) {
|
|
13
|
-
return new Promise((resolve) => {
|
|
14
|
-
execFile("git", args, { cwd, timeout: 15_000 }, (error, stdout) => {
|
|
15
|
-
resolve(error ? null : stdout.trim());
|
|
16
|
-
});
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
export async function createLaneWorktree(projectPath, laneId, env = process.env, isolate = false) {
|
|
20
|
-
const inPlace = { worktreePath: projectPath, branch: null, isolated: false };
|
|
21
|
-
if (!isolate)
|
|
22
|
-
return inPlace;
|
|
23
|
-
const isRepo = await git(["rev-parse", "--is-inside-work-tree"], projectPath);
|
|
24
|
-
if (isRepo !== "true")
|
|
25
|
-
return inPlace;
|
|
26
|
-
const branch = `${LANE_BRANCH_PREFIX}${laneId}`;
|
|
27
|
-
const worktreePath = path.join(lanesHome(env), "worktrees", laneId);
|
|
28
|
-
await mkdir(path.dirname(worktreePath), { recursive: true });
|
|
29
|
-
const added = await git(["worktree", "add", worktreePath, "-b", branch], projectPath);
|
|
30
|
-
if (added === null)
|
|
31
|
-
return inPlace;
|
|
32
|
-
return { worktreePath, branch, isolated: true };
|
|
33
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_WORKSPACE_EVENT_KEEP_COUNT = 500;
|
|
2
|
-
function mapRow(row) {
|
|
3
|
-
return {
|
|
4
|
-
bindingId: row.binding_id,
|
|
5
|
-
workspaceSequence: row.workspace_sequence,
|
|
6
|
-
eventType: row.event_type,
|
|
7
|
-
payloadJson: row.payload_json ?? null,
|
|
8
|
-
appliedAtMs: row.applied_at_ms ?? null,
|
|
9
|
-
createdAtMs: row.created_at_ms,
|
|
10
|
-
};
|
|
11
|
-
}
|
|
12
|
-
export function cacheWorkspaceEvents(store, bindingId, events) {
|
|
13
|
-
const now = Date.now();
|
|
14
|
-
const insert = store.db.prepare(`INSERT INTO cached_workspace_events (
|
|
15
|
-
binding_id, workspace_sequence, event_type, payload_json, applied_at_ms, created_at_ms
|
|
16
|
-
) VALUES (
|
|
17
|
-
:binding_id, :workspace_sequence, :event_type, :payload_json, :applied_at_ms, :created_at_ms
|
|
18
|
-
)
|
|
19
|
-
ON CONFLICT(binding_id, workspace_sequence) DO NOTHING`);
|
|
20
|
-
for (const event of events) {
|
|
21
|
-
insert.run({
|
|
22
|
-
binding_id: bindingId,
|
|
23
|
-
workspace_sequence: event.workspaceSequence,
|
|
24
|
-
event_type: event.eventType,
|
|
25
|
-
payload_json: event.payloadJson ?? null,
|
|
26
|
-
applied_at_ms: event.appliedAtMs ?? null,
|
|
27
|
-
created_at_ms: now,
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
export function latestCachedSequence(store, bindingId) {
|
|
32
|
-
const row = store.db
|
|
33
|
-
.prepare("SELECT MAX(workspace_sequence) AS latest FROM cached_workspace_events WHERE binding_id = :binding_id")
|
|
34
|
-
.get({ binding_id: bindingId });
|
|
35
|
-
return row?.latest ?? null;
|
|
36
|
-
}
|
|
37
|
-
export function listCachedWorkspaceEvents(store, bindingId) {
|
|
38
|
-
const rows = store.db
|
|
39
|
-
.prepare("SELECT * FROM cached_workspace_events WHERE binding_id = :binding_id ORDER BY workspace_sequence ASC")
|
|
40
|
-
.all({ binding_id: bindingId });
|
|
41
|
-
return rows.map(mapRow);
|
|
42
|
-
}
|
|
43
|
-
export function pruneWorkspaceEvents(store, bindingId, keepCount = DEFAULT_WORKSPACE_EVENT_KEEP_COUNT) {
|
|
44
|
-
store.db
|
|
45
|
-
.prepare(`DELETE FROM cached_workspace_events
|
|
46
|
-
WHERE binding_id = :binding_id
|
|
47
|
-
AND workspace_sequence NOT IN (
|
|
48
|
-
SELECT workspace_sequence FROM cached_workspace_events
|
|
49
|
-
WHERE binding_id = :binding_id
|
|
50
|
-
ORDER BY workspace_sequence DESC
|
|
51
|
-
LIMIT :keep
|
|
52
|
-
)`)
|
|
53
|
-
.run({ binding_id: bindingId, keep: keepCount });
|
|
54
|
-
}
|