@nowcrew/daemon 0.6.98 → 0.6.99-beta.1
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 +43 -1
- package/dist/automatic-daemon-installer.js +41 -10
- package/dist/automatic-service-migration.js +7 -6
- package/dist/cloud-report-upload.js +1 -10
- package/dist/computer-service.js +40 -42
- package/dist/computer-setup-client.js +43 -9
- package/dist/computer-setup-existing.js +154 -0
- package/dist/computer-setup-orchestration.js +32 -6
- package/dist/computer-setup.js +37 -6
- package/dist/computer-token-setup.js +46 -0
- package/dist/execution-journal.js +3 -1
- package/dist/execution-protocol.js +1 -1
- package/dist/execution-runner.js +4 -1
- package/dist/i18n.js +16 -0
- package/dist/local-execution-prompt.js +8 -3
- package/dist/local-executor.js +64 -39
- package/dist/local-memory/commit-telemetry.js +30 -0
- package/dist/local-memory/execution-workspace.js +62 -0
- package/dist/local-memory/full-index-search.js +47 -0
- package/dist/local-memory/index-budget.js +9 -3
- package/dist/local-memory/initialization-identity.js +36 -0
- package/dist/local-memory/inventory-cli.js +52 -8
- package/dist/local-memory/inventory-source.js +15 -8
- package/dist/local-memory/inventory-v2.js +362 -0
- package/dist/local-memory/legacy-adapter.js +233 -0
- package/dist/local-memory/migration-access.js +242 -0
- package/dist/local-memory/migration-cli.js +54 -0
- package/dist/local-memory/migration-io.js +199 -0
- package/dist/local-memory/migration-pending.js +9 -0
- package/dist/local-memory/migration-plan.js +432 -0
- package/dist/local-memory/migration-schema.js +55 -0
- package/dist/local-memory/migration-state-invariants.js +65 -0
- package/dist/local-memory/migration-store.js +414 -0
- package/dist/local-memory/migration-tool-entry.js +21 -0
- package/dist/local-memory/pending-query.js +5 -2
- package/dist/local-memory/pending-schema.js +1 -1
- package/dist/local-memory/pending.js +16 -2
- package/dist/local-memory/prompt.js +21 -5
- package/dist/local-memory/read-cli.js +98 -76
- package/dist/local-memory/rollout.js +79 -11
- package/dist/local-memory/runtime.js +181 -13
- package/dist/local-memory/source-path.js +13 -0
- package/dist/local-memory/storage-format.js +249 -0
- package/dist/local-memory/write.js +189 -21
- package/dist/local-memory/writer-mode.js +13 -0
- package/dist/local-memory-diagnostics.js +96 -15
- package/dist/local-memory-telemetry.js +20 -3
- package/dist/machine-info.js +6 -3
- package/dist/marketplace-controller.js +8 -21
- package/dist/marketplace-protocol.js +1 -1
- package/dist/marketplace-remote.js +1 -1
- package/dist/marketplace-runner.js +1 -1
- package/dist/marketplace-serve.js +2 -4
- package/dist/memory-migration-controller.js +89 -0
- package/dist/memory-migration-inspect.js +108 -0
- package/dist/memory-migration-journal.js +14 -0
- package/dist/memory-migration-protocol.js +14 -0
- package/dist/memory-migration-runtime.js +305 -0
- package/dist/memory-prune-diagnostics.js +60 -4
- package/dist/message-file-preview.js +11 -1
- package/dist/npm-command.js +5 -5
- package/dist/powershell-command.js +12 -0
- package/dist/project-skills/machine-hello-publisher.js +95 -6
- package/dist/prompt.js +9 -17
- package/dist/runner.js +1 -1
- package/dist/runtimes/claude-thread-runner.js +25 -4
- package/dist/serve.js +92 -10
- package/dist/token.js +1 -0
- package/dist/windows-scheduled-task.js +3 -3
- package/dist/windows-task-principal.js +26 -0
- package/dist/workspace-control-handler.js +16 -3
- package/dist/workspace.js +13 -2
- package/package.json +1 -1
- package/dist/marketplace-maintenance-bridge.js +0 -79
- package/dist/marketplace-maintenance-protocol.js +0 -40
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { access, readFile, readdir } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { posix, win32 } from "node:path";
|
|
4
|
+
import { resolveAutomaticInstallTarget } from "./automatic-install-target.js";
|
|
5
|
+
import { stableDaemonEntry } from "./automatic-daemon-installer.js";
|
|
6
|
+
import { resolveDaemonLocalHome } from "./daemon-local-home.js";
|
|
7
|
+
import { daemonGlobalInstallation } from "./daemon-installation.js";
|
|
8
|
+
import { isDaemonReleaseVersion } from "./daemon-release-version.js";
|
|
9
|
+
import { listProfiles, loadProfile, profileIsPrivate, profilePath } from "./computer-profile.js";
|
|
10
|
+
import { buildServiceSpec, captureExactService, systemCommandRunner, windowsProfileIsPrivate } from "./computer-service.js";
|
|
11
|
+
import { buildManagedServiceRecord } from "./managed-service-lifecycle.js";
|
|
12
|
+
import { managedServiceIdentityMatches, readManagedServiceRegistry } from "./managed-service-registry.js";
|
|
13
|
+
import { serverUrlsMatch } from "./server-url.js";
|
|
14
|
+
import { assertBootstrapMatches, localPathsMatch } from "./computer-setup-orchestration.js";
|
|
15
|
+
const storageKeys = ["NOWWORK_HOME", "NOWWORK_ENV_TAG", "NOWWORK_STORAGE_POLICY_PATH", "NOWWORK_LOCAL_HOME", "NOWWORK_LOCAL_HOME_MODE"];
|
|
16
|
+
const decodeXml = (value) => value.replaceAll(""", '"').replaceAll("'", "'")
|
|
17
|
+
.replaceAll("<", "<").replaceAll(">", ">").replaceAll("&", "&");
|
|
18
|
+
// Only recover the generated environment fields; the rebuilt descriptor must still match byte-for-byte.
|
|
19
|
+
function descriptorEnvironment(descriptor, platform) {
|
|
20
|
+
return Object.fromEntries(storageKeys.flatMap((key) => {
|
|
21
|
+
const value = platform === "darwin"
|
|
22
|
+
? new RegExp(`<key>${key}</key><string>([^<]*)</string>`, "u").exec(descriptor)?.[1]
|
|
23
|
+
: new RegExp(`^Environment="${key}=((?:[^"\\\\]|\\\\.)*)"$`, "mu").exec(descriptor)?.[1];
|
|
24
|
+
return value === undefined ? [] : [[key, platform === "darwin" ? decodeXml(value) : value.replace(/\\(.)/gu, "$1").replaceAll("%%", "%")]];
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
async function windowsNodePath(serviceId, runner) {
|
|
28
|
+
const script = "$ErrorActionPreference='Stop';$n=[Text.Encoding]::Unicode.GetString([Convert]::FromBase64String($args[0]));"
|
|
29
|
+
+ "$t=@(Get-ScheduledTask -ErrorAction Stop|Where-Object{$_.TaskPath -eq '\\' -and $_.TaskName -ceq $n});"
|
|
30
|
+
+ "if($t.Count -ne 1){throw 'scheduled task identity unknown'};$a=@($t[0].Actions);"
|
|
31
|
+
+ "if($a.Count -ne 1){throw 'scheduled task action unknown'};$a[0].Execute|ConvertTo-Json -Compress;";
|
|
32
|
+
const result = await runner("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script,
|
|
33
|
+
Buffer.from(serviceId, "utf16le").toString("base64")]);
|
|
34
|
+
const path = result.exitCode === 0 ? JSON.parse(result.stdout) : null;
|
|
35
|
+
if (typeof path !== "string" || !win32.isAbsolute(path))
|
|
36
|
+
throw new Error("Scheduled task executable unavailable");
|
|
37
|
+
return path;
|
|
38
|
+
}
|
|
39
|
+
/** One-shot inspection only. Never consumes credentials or changes local/service state. */
|
|
40
|
+
export async function inspectExistingComputerSetup(input, client, options = {}) {
|
|
41
|
+
if (input.args.setupToken === undefined)
|
|
42
|
+
return { status: "unavailable" };
|
|
43
|
+
const platform = options.platform ?? process.platform;
|
|
44
|
+
const userHome = options.userHome ?? homedir();
|
|
45
|
+
const environment = options.environment ?? process.env;
|
|
46
|
+
const runner = options.runner ?? systemCommandRunner;
|
|
47
|
+
const paths = platform === "win32" ? win32 : posix;
|
|
48
|
+
try {
|
|
49
|
+
const requestedLocalHome = environment.NOWWORK_LOCAL_HOME;
|
|
50
|
+
const requestedLocalHomeMode = environment.NOWWORK_LOCAL_HOME_MODE ?? environment.NOWWORK_STORAGE_MODE;
|
|
51
|
+
const localOptions = {
|
|
52
|
+
...(requestedLocalHome === undefined ? {} : { requestedLocalHome }),
|
|
53
|
+
...(requestedLocalHomeMode === undefined ? {} : { requestedLocalHomeMode }),
|
|
54
|
+
};
|
|
55
|
+
const configuredHome = resolveDaemonLocalHome({ userHome, platform, profile: "in", mode: "nowwork",
|
|
56
|
+
...(requestedLocalHome === undefined ? {} : { explicit: requestedLocalHome }) }).daemonHome;
|
|
57
|
+
const legacyRoot = paths.join(userHome, ".crew");
|
|
58
|
+
const legacyEntries = await readdir(legacyRoot, { withFileTypes: true }).catch((error) => {
|
|
59
|
+
if (error.code === "ENOENT")
|
|
60
|
+
return [];
|
|
61
|
+
throw error;
|
|
62
|
+
});
|
|
63
|
+
const homes = [...new Set([configuredHome, paths.join(userHome, ".nowwork", "config"),
|
|
64
|
+
...legacyEntries.filter((entry) => entry.isDirectory() && /^daemon(?:-[a-z0-9_-]+)?$/u.test(entry.name))
|
|
65
|
+
.map((entry) => paths.join(legacyRoot, entry.name))])];
|
|
66
|
+
const candidates = [];
|
|
67
|
+
for (const home of homes) {
|
|
68
|
+
for (const name of await listProfiles(home)) {
|
|
69
|
+
const privateProfile = platform === "win32"
|
|
70
|
+
? await windowsProfileIsPrivate(profilePath(name, home), runner)
|
|
71
|
+
: await profileIsPrivate(name, home, platform);
|
|
72
|
+
if (!privateProfile)
|
|
73
|
+
continue;
|
|
74
|
+
const profile = await loadProfile(name, home, { platform });
|
|
75
|
+
if (profile.name !== name || profile.machineId !== input.args.machineId || profile.workspaceId === undefined
|
|
76
|
+
|| !serverUrlsMatch(profile.serverUrl, input.serverUrl)
|
|
77
|
+
|| (input.args.workspace.kind === "id" && profile.workspaceId !== input.args.workspace.value))
|
|
78
|
+
continue;
|
|
79
|
+
const target = resolveAutomaticInstallTarget({ serverUrl: input.serverUrl, requestedProfile: name,
|
|
80
|
+
userHome, platform, ...localOptions });
|
|
81
|
+
if (!localPathsMatch(target.daemonHome, home, platform))
|
|
82
|
+
continue;
|
|
83
|
+
if (profile.agentsRoot === undefined || !localPathsMatch(profile.agentsRoot, target.agentsRoot, platform))
|
|
84
|
+
continue;
|
|
85
|
+
candidates.push({ profile, target });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (candidates.length !== 1)
|
|
89
|
+
return { status: "unavailable" };
|
|
90
|
+
const { profile, target } = candidates[0];
|
|
91
|
+
const receipt = await client.receipt({ setupToken: input.args.setupToken, machineToken: profile.machineToken });
|
|
92
|
+
assertBootstrapMatches(receipt.target, input);
|
|
93
|
+
if (receipt.target.workspaceId !== profile.workspaceId || receipt.target.profile !== profile.name)
|
|
94
|
+
return { status: "unavailable" };
|
|
95
|
+
if (receipt.credentialState === "pending")
|
|
96
|
+
return { status: "pending" };
|
|
97
|
+
const connection = receipt.connection;
|
|
98
|
+
if (receipt.credentialState !== "finalized" || !receipt.connectionConfirmed || connection.status !== "online"
|
|
99
|
+
|| !connection.credentialMatchesConnection || connection.epoch <= receipt.target.minimumConnectionEpoch
|
|
100
|
+
|| connection.profile !== profile.name || connection.daemonHome === null || connection.agentsRoot === null
|
|
101
|
+
|| !localPathsMatch(connection.daemonHome, target.daemonHome, platform)
|
|
102
|
+
|| !localPathsMatch(connection.agentsRoot, target.agentsRoot, platform)
|
|
103
|
+
|| (receipt.target.storageId !== undefined && receipt.target.storageId !== connection.storageId))
|
|
104
|
+
return { status: "unavailable" };
|
|
105
|
+
const localHome = paths.resolve(target.npmPrefix, "..", "..");
|
|
106
|
+
const entryPath = stableDaemonEntry(target, platform);
|
|
107
|
+
const installation = daemonGlobalInstallation(entryPath, platform);
|
|
108
|
+
if (installation === null)
|
|
109
|
+
return { status: "unavailable" };
|
|
110
|
+
await access(entryPath);
|
|
111
|
+
const packageJson = JSON.parse(await readFile(paths.join(installation.packageRoot, "package.json"), "utf8"));
|
|
112
|
+
if (!packageJson || typeof packageJson !== "object" || !("name" in packageJson) || packageJson.name !== "@nowcrew/daemon"
|
|
113
|
+
|| !("version" in packageJson) || typeof packageJson.version !== "string" || !isDaemonReleaseVersion(packageJson.version))
|
|
114
|
+
return { status: "unavailable" };
|
|
115
|
+
const base = { platform, profile: profile.name, profileHome: target.daemonHome, userHome,
|
|
116
|
+
uid: options.uid ?? process.getuid?.(), entryPath };
|
|
117
|
+
let spec;
|
|
118
|
+
if (platform === "win32") {
|
|
119
|
+
const identity = buildServiceSpec({ ...base, nodePath: process.execPath, environment: {} });
|
|
120
|
+
const nodePath = await windowsNodePath(identity.id, runner);
|
|
121
|
+
await access(nodePath);
|
|
122
|
+
spec = buildServiceSpec({ ...base, nodePath, environment: {} });
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
const records = (await readManagedServiceRegistry({ userHome, localHome })).filter((record) => record.profile === profile.name
|
|
126
|
+
&& localPathsMatch(record.daemonHome, target.daemonHome, platform));
|
|
127
|
+
if (records.length !== 1)
|
|
128
|
+
return { status: "unavailable" };
|
|
129
|
+
const record = records[0];
|
|
130
|
+
await access(record.nodePath);
|
|
131
|
+
const descriptor = await readFile(record.descriptorPath, "utf8");
|
|
132
|
+
spec = buildServiceSpec({ ...base, nodePath: record.nodePath, entryPath: record.entryPath,
|
|
133
|
+
environment: descriptorEnvironment(descriptor, platform) });
|
|
134
|
+
const expected = buildManagedServiceRecord({ spec, daemonHome: target.daemonHome, agentsRoot: target.agentsRoot,
|
|
135
|
+
nodePath: record.nodePath, entryPath, ...installation });
|
|
136
|
+
if (!managedServiceIdentityMatches(record, expected))
|
|
137
|
+
return { status: "unavailable" };
|
|
138
|
+
}
|
|
139
|
+
const service = await captureExactService(spec, runner);
|
|
140
|
+
if (!service.installed || !service.exact || !service.running)
|
|
141
|
+
return { status: "unavailable" };
|
|
142
|
+
return { status: "connected", result: {
|
|
143
|
+
workspaceId: receipt.target.workspaceId, workspaceSlug: receipt.target.workspaceSlug, machineId: receipt.target.machineId,
|
|
144
|
+
environment: receipt.target.environment, profile: profile.name, daemonVersion: packageJson.version,
|
|
145
|
+
serviceId: spec.id, serviceStatus: "running", helloConfirmed: true, localNowworkHome: localHome,
|
|
146
|
+
daemonHome: target.daemonHome, agentsRoot: target.agentsRoot,
|
|
147
|
+
...(connection.storageId === undefined ? {} : { storageId: connection.storageId }),
|
|
148
|
+
} };
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// Failed inspection is not proof of installation, connection, or credential ownership.
|
|
152
|
+
return { status: "unavailable" };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { computerSetupRecoveryGuidance, redactSetupSecrets, } from "./computer-setup-client.js";
|
|
2
|
+
import { automaticDaemonInstallStageMessage } from "./automatic-daemon-installer.js";
|
|
3
|
+
import { inspectExistingComputerSetup } from "./computer-setup-existing.js";
|
|
2
4
|
import { isProductionServerHostname, serverUrlsMatch } from "./server-url.js";
|
|
3
5
|
import { posix, win32 } from "node:path";
|
|
4
6
|
import { resolveAgentsRoot } from "./computer-profile.js";
|
|
5
|
-
function safeError(error, machineToken) {
|
|
6
|
-
const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), machineToken
|
|
7
|
-
return new Error(message
|
|
7
|
+
function safeError(error, machineToken, setupToken) {
|
|
8
|
+
const message = redactSetupSecrets(error instanceof Error ? error.message : String(error), [machineToken ?? "", setupToken ?? ""]);
|
|
9
|
+
return new Error(message);
|
|
8
10
|
}
|
|
9
|
-
function assertBootstrapMatches(bootstrap, input) {
|
|
11
|
+
export function assertBootstrapMatches(bootstrap, input) {
|
|
10
12
|
if (bootstrap.machineId !== input.args.machineId) {
|
|
11
13
|
throw new Error("setup bootstrap machine does not match --machine");
|
|
12
14
|
}
|
|
@@ -40,7 +42,7 @@ function secondsUntil(expiresAt, now) {
|
|
|
40
42
|
const remaining = Date.parse(expiresAt) - now();
|
|
41
43
|
return Math.max(0, Math.ceil(remaining / 1_000));
|
|
42
44
|
}
|
|
43
|
-
function localPathsMatch(left, right, platform) {
|
|
45
|
+
export function localPathsMatch(left, right, platform) {
|
|
44
46
|
const paths = platform === "win32" ? win32 : posix;
|
|
45
47
|
if (!paths.isAbsolute(left) || !paths.isAbsolute(right))
|
|
46
48
|
return false;
|
|
@@ -51,6 +53,26 @@ function localPathsMatch(left, right, platform) {
|
|
|
51
53
|
return normalize(left) === normalize(right);
|
|
52
54
|
}
|
|
53
55
|
async function obtainBootstrap(input, deps) {
|
|
56
|
+
if (input.args.setupToken !== undefined) {
|
|
57
|
+
deps.reportPairing("Checking computer connection information...");
|
|
58
|
+
const result = await deps.client.poll(input.args.setupToken);
|
|
59
|
+
if (result.status !== "authorized") {
|
|
60
|
+
deps.reportPairing("Checking the existing computer connection...");
|
|
61
|
+
const inspection = await (deps.inspectExistingSetup ?? ((candidate, client) => inspectExistingComputerSetup(candidate, client, {
|
|
62
|
+
...(deps.platform === undefined ? {} : { platform: deps.platform }),
|
|
63
|
+
...(deps.environment === undefined ? {} : { environment: deps.environment }),
|
|
64
|
+
})))(input, deps.client);
|
|
65
|
+
if (inspection.status === "connected") {
|
|
66
|
+
deps.reportPairing("This computer is already connected. No installation changes are needed.");
|
|
67
|
+
return inspection.result;
|
|
68
|
+
}
|
|
69
|
+
if (inspection.status === "pending") {
|
|
70
|
+
throw new Error("Setup is still in progress; check the original terminal and wait for it to finish");
|
|
71
|
+
}
|
|
72
|
+
throw new Error("Setup command expired or unavailable; regenerate it in NowWork and run the new command");
|
|
73
|
+
}
|
|
74
|
+
return result.bootstrap;
|
|
75
|
+
}
|
|
54
76
|
const pairing = await deps.client.start({
|
|
55
77
|
workspace: input.args.workspace,
|
|
56
78
|
machineId: input.args.machineId,
|
|
@@ -112,11 +134,14 @@ export async function runComputerSetupOrchestration(input, deps) {
|
|
|
112
134
|
};
|
|
113
135
|
try {
|
|
114
136
|
const authorized = await obtainBootstrap(input, deps);
|
|
137
|
+
if ("helloConfirmed" in authorized)
|
|
138
|
+
return authorized;
|
|
115
139
|
const token = authorized.machineToken;
|
|
116
140
|
bootstrap = authorized;
|
|
117
141
|
machineToken = token;
|
|
118
142
|
assertBootstrapMatches(authorized, input);
|
|
119
143
|
const installation = await deps.installAutomatically({
|
|
144
|
+
onProgress: (stage) => deps.reportPairing(automaticDaemonInstallStageMessage[stage]),
|
|
120
145
|
serverUrl: authorized.serverUrl,
|
|
121
146
|
machineToken: token,
|
|
122
147
|
workspaceId: authorized.workspaceId,
|
|
@@ -174,6 +199,7 @@ export async function runComputerSetupOrchestration(input, deps) {
|
|
|
174
199
|
});
|
|
175
200
|
return {
|
|
176
201
|
workspaceId: authorized.workspaceId,
|
|
202
|
+
...(authorized.workspaceSlug === undefined ? {} : { workspaceSlug: authorized.workspaceSlug }),
|
|
177
203
|
machineId: authorized.machineId,
|
|
178
204
|
environment: authorized.environment,
|
|
179
205
|
profile: installation.profile,
|
|
@@ -198,6 +224,6 @@ export async function runComputerSetupOrchestration(input, deps) {
|
|
|
198
224
|
+ `local recovery blocked. ${computerSetupRecoveryGuidance(bootstrap)}`, { cause: error });
|
|
199
225
|
}
|
|
200
226
|
}
|
|
201
|
-
throw safeError(error, machineToken);
|
|
227
|
+
throw safeError(error, machineToken, input.args.setupToken);
|
|
202
228
|
}
|
|
203
229
|
}
|
package/dist/computer-setup.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { DEFAULT_SERVER_URL } from "./config.js";
|
|
4
|
+
import { MachineTokenSchema } from "./computer-profile.js";
|
|
4
5
|
import { installDaemonAutomatically, } from "./automatic-daemon-installer.js";
|
|
5
6
|
import { createComputerSetupClient, openSetupBrowser, redactSetupSecrets, } from "./computer-setup-client.js";
|
|
6
7
|
import { runComputerSetupOrchestration, } from "./computer-setup-orchestration.js";
|
|
7
8
|
import { detectDaemonLang, formatDaemonText } from "./i18n.js";
|
|
8
9
|
import { systemCommandRunner } from "./computer-service.js";
|
|
10
|
+
import { runMachineTokenSetup } from "./computer-token-setup.js";
|
|
9
11
|
const WorkspaceSlugSchema = z.string()
|
|
10
12
|
.min(2)
|
|
11
13
|
.max(40)
|
|
@@ -70,6 +72,8 @@ export function parseSetupArgs(argv) {
|
|
|
70
72
|
assertRepeatedOptionValuesAgree(argv, "machine", (value) => MachineIdSchema.parse(value).toLowerCase());
|
|
71
73
|
assertRepeatedOptionValuesAgree(argv, "machine-id", (value) => MachineIdSchema.parse(value).toLowerCase());
|
|
72
74
|
assertRepeatedOptionValuesAgree(argv, "server-url", normalizedServerOrigin);
|
|
75
|
+
assertRepeatedOptionValuesAgree(argv, "setup-token", (value) => value);
|
|
76
|
+
assertRepeatedOptionValuesAgree(argv, "api-key", (value) => value);
|
|
73
77
|
const parsed = parseArgs({
|
|
74
78
|
args: [...argv],
|
|
75
79
|
allowPositionals: true,
|
|
@@ -82,6 +86,8 @@ export function parseSetupArgs(argv) {
|
|
|
82
86
|
"server-url": { type: "string" },
|
|
83
87
|
json: { type: "boolean", default: false },
|
|
84
88
|
"no-browser": { type: "boolean", default: false },
|
|
89
|
+
"setup-token": { type: "string" },
|
|
90
|
+
"api-key": { type: "string" },
|
|
85
91
|
help: { type: "boolean", short: "h", default: false },
|
|
86
92
|
},
|
|
87
93
|
});
|
|
@@ -124,6 +130,14 @@ export function parseSetupArgs(argv) {
|
|
|
124
130
|
}
|
|
125
131
|
const machineId = parsedMachine;
|
|
126
132
|
const serverUrl = stringValue(values, "server-url");
|
|
133
|
+
const setupToken = stringValue(values, "setup-token");
|
|
134
|
+
const machineToken = stringValue(values, "api-key");
|
|
135
|
+
if (setupToken !== undefined && machineToken !== undefined)
|
|
136
|
+
throw new Error("Choose one setup credential source");
|
|
137
|
+
if (setupToken !== undefined && !/^[A-Za-z0-9_-]{32,512}$/u.test(setupToken))
|
|
138
|
+
throw new Error("Invalid setup Token");
|
|
139
|
+
if (machineToken !== undefined)
|
|
140
|
+
MachineTokenSchema.parse(machineToken);
|
|
127
141
|
return Object.freeze({
|
|
128
142
|
command: "setup",
|
|
129
143
|
workspace,
|
|
@@ -131,6 +145,8 @@ export function parseSetupArgs(argv) {
|
|
|
131
145
|
...(serverUrl === undefined ? {} : { serverUrl: ServerUrlSchema.parse(serverUrl) }),
|
|
132
146
|
...(booleanValue(values, "json") ? { json: true } : {}),
|
|
133
147
|
...(booleanValue(values, "no-browser") ? { noBrowser: true } : {}),
|
|
148
|
+
...(setupToken === undefined ? {} : { setupToken }),
|
|
149
|
+
...(machineToken === undefined ? {} : { machineToken }),
|
|
134
150
|
});
|
|
135
151
|
}
|
|
136
152
|
function resolveSetupServerUrl(args, env) {
|
|
@@ -148,7 +164,8 @@ export function setupUsage(td = (message) => message) {
|
|
|
148
164
|
" nowwork-computer setup --workspace /workspace --machine <uuid>",
|
|
149
165
|
" nowwork-computer setup --workspace-id <uuid> --machine-id <uuid>",
|
|
150
166
|
"",
|
|
151
|
-
"Options: --server-url <url> --json --no-browser",
|
|
167
|
+
"Options: --server-url <url> --setup-token <token> --api-key <sk_machine_*> --json --no-browser",
|
|
168
|
+
td("Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported."),
|
|
152
169
|
].join("\n") + "\n";
|
|
153
170
|
}
|
|
154
171
|
function defaults() {
|
|
@@ -167,6 +184,7 @@ function defaults() {
|
|
|
167
184
|
}
|
|
168
185
|
function humanSummary(result, td) {
|
|
169
186
|
return [
|
|
187
|
+
td("Connected to {{workspace}}.", { workspace: result.workspaceSlug ?? result.workspaceId }),
|
|
170
188
|
td("Workspace: {{value}}", { value: result.workspaceId }),
|
|
171
189
|
td("Machine: {{value}}", { value: result.machineId }),
|
|
172
190
|
td("Environment: {{value}}", { value: result.environment }),
|
|
@@ -205,6 +223,7 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
|
|
|
205
223
|
return null;
|
|
206
224
|
const deps = { ...defaults(), ...overrides };
|
|
207
225
|
const lang = detectDaemonLang(deps.env);
|
|
226
|
+
const secrets = [...optionOccurrences(argv, "setup-token"), ...optionOccurrences(argv, "api-key"), deps.env.CREW_MACHINE_TOKEN ?? ""];
|
|
208
227
|
try {
|
|
209
228
|
if (argv.includes("--help") || argv.includes("-h")) {
|
|
210
229
|
deps.stdout(setupUsage((message, values) => formatDaemonText(lang, message, values)));
|
|
@@ -213,32 +232,44 @@ export async function runComputerSetupCommand(argv, overrides = {}) {
|
|
|
213
232
|
let args;
|
|
214
233
|
try {
|
|
215
234
|
args = parseSetupArgs(argv);
|
|
235
|
+
const envToken = deps.env.CREW_MACHINE_TOKEN?.trim();
|
|
236
|
+
if (envToken) {
|
|
237
|
+
if (args.setupToken !== undefined || (args.machineToken !== undefined && args.machineToken !== envToken)) {
|
|
238
|
+
throw new Error("Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token");
|
|
239
|
+
}
|
|
240
|
+
args = Object.freeze({ ...args, machineToken: MachineTokenSchema.parse(envToken) });
|
|
241
|
+
}
|
|
216
242
|
}
|
|
217
243
|
catch (error) {
|
|
218
|
-
deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
244
|
+
deps.stderr(`nowwork-computer: ${formatDaemonText(lang, redactSetupSecrets(error.message, secrets))}\n`);
|
|
219
245
|
return 2;
|
|
220
246
|
}
|
|
221
|
-
if (args.json === true) {
|
|
247
|
+
if (args.json === true && args.setupToken === undefined && args.machineToken === undefined) {
|
|
222
248
|
deps.stderr(`${formatDaemonText(lang, "Starting computer setup pairing...")}\n`);
|
|
223
249
|
}
|
|
224
250
|
const serverUrl = resolveSetupServerUrl(args, deps.env);
|
|
225
251
|
const createClient = overrides.createClient
|
|
226
252
|
?? ((value) => createComputerSetupClient({ serverUrl: value, fetch: deps.fetch }));
|
|
227
|
-
const
|
|
253
|
+
const orchestrationDeps = {
|
|
228
254
|
client: createClient(serverUrl),
|
|
229
255
|
installAutomatically: deps.installAutomatically,
|
|
230
256
|
openBrowser: args.noBrowser === true ? async () => { } : deps.openBrowser,
|
|
231
257
|
sleep: deps.sleep,
|
|
232
258
|
reportPairing: (message) => deps[args.json === true ? "stderr" : "stdout"](`${localizedPairingMessage(lang, message)}\n`),
|
|
233
259
|
now: deps.now,
|
|
234
|
-
|
|
260
|
+
platform: deps.platform,
|
|
261
|
+
environment: deps.env,
|
|
262
|
+
};
|
|
263
|
+
const result = args.machineToken === undefined
|
|
264
|
+
? await runComputerSetupOrchestration({ args, serverUrl }, orchestrationDeps)
|
|
265
|
+
: await runMachineTokenSetup({ args, serverUrl }, orchestrationDeps, args.machineToken);
|
|
235
266
|
deps.stdout(args.json === true
|
|
236
267
|
? `${JSON.stringify(result)}\n`
|
|
237
268
|
: humanSummary(result, (message, values) => formatDaemonText(lang, message, values)));
|
|
238
269
|
return 0;
|
|
239
270
|
}
|
|
240
271
|
catch (error) {
|
|
241
|
-
deps.stderr(`nowwork-computer: ${redactSetupSecrets(error.message)}\n`);
|
|
272
|
+
deps.stderr(`nowwork-computer: ${formatDaemonText(lang, redactSetupSecrets(error.message, secrets))}\n`);
|
|
242
273
|
return 1;
|
|
243
274
|
}
|
|
244
275
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { automaticDaemonInstallStageMessage } from "./automatic-daemon-installer.js";
|
|
2
|
+
import { assertBootstrapMatches, localPathsMatch } from "./computer-setup-orchestration.js";
|
|
3
|
+
import { redactSetupSecrets } from "./computer-setup-client.js";
|
|
4
|
+
export async function runMachineTokenSetup(input, deps, machineToken) {
|
|
5
|
+
try {
|
|
6
|
+
deps.reportPairing("Checking computer connection information...");
|
|
7
|
+
const target = await deps.client.tokenTarget(machineToken);
|
|
8
|
+
assertBootstrapMatches(target, input);
|
|
9
|
+
const installation = await deps.installAutomatically({
|
|
10
|
+
onProgress: (stage) => deps.reportPairing(automaticDaemonInstallStageMessage[stage]),
|
|
11
|
+
serverUrl: target.serverUrl, machineToken, workspaceId: target.workspaceId, machineId: target.machineId,
|
|
12
|
+
requestedProfile: target.profile,
|
|
13
|
+
...(target.releaseTag === undefined ? {} : { requestedReleaseTag: target.releaseTag }),
|
|
14
|
+
...(target.registryUrl === undefined ? {} : { requestedRegistryUrl: target.registryUrl }),
|
|
15
|
+
verifyAfterStart: async (candidate) => {
|
|
16
|
+
const deadline = deps.now() + 120_000;
|
|
17
|
+
for (;;) {
|
|
18
|
+
const current = await deps.client.tokenTarget(machineToken);
|
|
19
|
+
assertBootstrapMatches(current, input);
|
|
20
|
+
const connection = current.connection;
|
|
21
|
+
const platform = deps.platform ?? process.platform;
|
|
22
|
+
if (connection.status === "online" && connection.epoch > target.minimumConnectionEpoch
|
|
23
|
+
&& connection.credentialMatchesConnection && current.environment === target.environment
|
|
24
|
+
&& connection.profile === candidate.profile
|
|
25
|
+
&& connection.daemonHome !== null && localPathsMatch(connection.daemonHome, candidate.daemonHome, platform)
|
|
26
|
+
&& connection.agentsRoot !== null && localPathsMatch(connection.agentsRoot, candidate.agentsRoot, platform)
|
|
27
|
+
&& (target.storageId === undefined || connection.storageId === target.storageId))
|
|
28
|
+
return;
|
|
29
|
+
if (deps.now() >= deadline)
|
|
30
|
+
throw new Error("Timed out confirming the new computer connection");
|
|
31
|
+
await deps.sleep(Math.min(2_000, Math.max(0, deadline - deps.now())));
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return {
|
|
36
|
+
workspaceId: target.workspaceId, workspaceSlug: target.workspaceSlug, machineId: target.machineId,
|
|
37
|
+
environment: target.environment, profile: installation.profile, daemonVersion: installation.version,
|
|
38
|
+
serviceId: installation.serviceId, serviceStatus: "running", helloConfirmed: true,
|
|
39
|
+
localNowworkHome: installation.localHome, daemonHome: installation.daemonHome, agentsRoot: installation.agentsRoot,
|
|
40
|
+
...(target.storageId === undefined ? {} : { storageId: target.storageId }),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
throw new Error(redactSetupSecrets(error instanceof Error ? error.message : String(error), [machineToken]));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -8,7 +8,7 @@ import { provePosixProcessGroupStopped } from "./execution-posix-stop-proof.js";
|
|
|
8
8
|
export { JournalLockedError, JournalLockCorruptionError } from "./execution-journal-lock.js";
|
|
9
9
|
const ExecutionIdSchema = z.string().uuid();
|
|
10
10
|
const TimestampSchema = z.string().datetime({ offset: true });
|
|
11
|
-
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "pi"]);
|
|
11
|
+
const RuntimeSchema = z.enum(["claude", "codex", "kimi", "hermes", "opencode", "deepseek-harness", "memory_migration", "pi"]);
|
|
12
12
|
const RUNTIME_READY_SUFFIX = ".runtime-ready";
|
|
13
13
|
const RawJournalEntrySchema = z.object({
|
|
14
14
|
executionId: ExecutionIdSchema,
|
|
@@ -523,6 +523,8 @@ export function createExecutionJournal(agentsRoot, options = {}) {
|
|
|
523
523
|
}
|
|
524
524
|
};
|
|
525
525
|
const pruneInternal = async () => {
|
|
526
|
+
if (options.retainCompleted)
|
|
527
|
+
return;
|
|
526
528
|
const protectedExecutionIds = new Set(options.protectedExecutionIds?.() ?? []);
|
|
527
529
|
const entries = await readAll();
|
|
528
530
|
const acknowledged = entries
|
|
@@ -319,7 +319,7 @@ const RawExecutionCompletedSchema = z.object({
|
|
|
319
319
|
providerErrorCategory: ProviderErrorCategorySchema.optional(),
|
|
320
320
|
providerHttpStatus: ProviderHttpStatusSchema.optional(),
|
|
321
321
|
providerErrorCode: ProviderErrorCodeSchema.optional(),
|
|
322
|
-
runtime: RuntimeSchema,
|
|
322
|
+
runtime: z.union([RuntimeSchema, z.literal("memory_migration")]),
|
|
323
323
|
model: z.string().optional(),
|
|
324
324
|
resumed: z.boolean(),
|
|
325
325
|
finalText: z.string().optional(),
|
package/dist/execution-runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { isLocalMemoryV2Enabled, localMemoryV2Environment, resolveReposRoot } from "./config.js";
|
|
4
|
+
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
4
5
|
import { DaemonToServerExecutionFrameSchema, ExecutionCompletedSchema, ExecutionRejectedSchema, ExecutionStartSchema, } from "./execution-protocol.js";
|
|
5
6
|
import { JournalConflictError } from "./execution-journal.js";
|
|
6
7
|
import { boundExecutionFrame } from "./execution-event-limit.js";
|
|
@@ -846,8 +847,9 @@ export async function runExecution(config, input, dependencies) {
|
|
|
846
847
|
const localMemoryGuardInBase = localMemoryV2Environment(config.serverUrl) === "custom";
|
|
847
848
|
const boundedSystemPrompt = (context) => {
|
|
848
849
|
const enabled = context.workspace.localMemoryV2Enabled ?? localMemoryV2Enabled;
|
|
849
|
-
const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
|
|
850
|
+
const systemPromptWithLocalFacts = withLocalExecutionFacts(`${spec.instructions.systemPrompt}${enabled && localMemoryGuardInBase && detectLocalMemoryStorageVersion(context.workspace.memory) !== 2 ? renderLocalMemoryV2WriteGuard() : ""}`, baseSystemPromptBudget, {
|
|
850
851
|
...(selfTraining ? { isolateLocalMemory: true } : {}),
|
|
852
|
+
memoryQuery: spec.instructions.wakePrompt,
|
|
851
853
|
collaborationTools: config.codexCollaborationTools && spec.runtime.name === "codex",
|
|
852
854
|
});
|
|
853
855
|
const base = typeof systemPromptWithLocalFacts === "string"
|
|
@@ -887,6 +889,7 @@ export async function runExecution(config, input, dependencies) {
|
|
|
887
889
|
serverUrl: config.serverUrl,
|
|
888
890
|
token: credential.token,
|
|
889
891
|
agentId: credential.agentId,
|
|
892
|
+
...(dependencies.controlIdentity === undefined ? {} : dependencies.controlIdentity),
|
|
890
893
|
agentsRoot: config.agentsRoot,
|
|
891
894
|
reposRoot: resolveReposRoot(config.agentsRoot, config.reposRoot),
|
|
892
895
|
...(config.qaReportRoot === undefined ? {} : { qaReportRoot: config.qaReportRoot }),
|
package/dist/i18n.js
CHANGED
|
@@ -36,6 +36,22 @@ const zh = {
|
|
|
36
36
|
"Code {{code}}": "验证码 {{code}}",
|
|
37
37
|
"Browser open skipped: {{reason}}": "未能自动打开浏览器:{{reason}}",
|
|
38
38
|
"Starting computer setup pairing...": "正在发起电脑接入授权…",
|
|
39
|
+
"Token setup does not require browser authorization. CREW_MACHINE_TOKEN is also supported.": "Token 接入无需浏览器授权,也支持 CREW_MACHINE_TOKEN。",
|
|
40
|
+
"Checking computer connection information...": "正在检查接入信息…",
|
|
41
|
+
"Checking the existing computer connection...": "正在检查已有电脑连接…",
|
|
42
|
+
"Downloading the Daemon package...": "正在下载 Daemon 安装包…",
|
|
43
|
+
"Configuring the Daemon service...": "正在配置 Daemon 服务…",
|
|
44
|
+
"Starting the Daemon service...": "正在启动 Daemon 服务…",
|
|
45
|
+
"This computer is already connected. No installation changes are needed.": "此电脑已经连接,无需修改安装。",
|
|
46
|
+
"Setup is still in progress; check the original terminal and wait for it to finish": "接入仍在进行中,请查看原来的终端并等待接入完成。",
|
|
47
|
+
"Downloading, installing, and starting the Daemon service...": "正在下载、安装并启动 Daemon 服务…",
|
|
48
|
+
"Confirming the computer connection...": "正在确认电脑连接…",
|
|
49
|
+
"Connected to {{workspace}}.": "已接入 {{workspace}}。",
|
|
50
|
+
"Setup command expired or unavailable; regenerate it in NowWork and run the new command": "接入命令已过期或不可用,请在 NowWork 重新生成并执行新命令。",
|
|
51
|
+
"Timed out confirming the new computer connection": "确认新连接超时,请检查网络和服务状态。",
|
|
52
|
+
"Invalid setup Token": "接入 Token 无效。",
|
|
53
|
+
"Choose one setup credential source": "请只使用一种接入凭据。",
|
|
54
|
+
"Choose one setup credential source; clear CREW_MACHINE_TOKEN when using another Token": "请只使用一种接入凭据;使用其他 Token 时请清除 CREW_MACHINE_TOKEN。",
|
|
39
55
|
"Workspace: {{value}}": "工作区:{{value}}",
|
|
40
56
|
"Machine: {{value}}": "机器: {{value}}",
|
|
41
57
|
"Environment: {{value}}": "环境:{{value}}",
|
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* 本机执行提示词的合成与预算裁剪。
|
|
3
3
|
*
|
|
4
4
|
* server 提示词是权威部分,绝不裁剪;项目工作区提示词是绑定执行的强制部分,放不下就直接失败;
|
|
5
|
-
*
|
|
5
|
+
* 本机事实与旧格式预览可按预算省略;schema2身份和置顶规则必须保留,放不下则拒绝执行。
|
|
6
6
|
*/
|
|
7
|
+
import { detectLocalMemoryStorageVersion } from "./local-memory/storage-format.js";
|
|
7
8
|
import { buildCollaborationToolsPrompt, buildRuntimeWorkspacePrompt, capMemoryForInject, capWorkLogForInject, } from "./prompt.js";
|
|
8
9
|
const trainingProjectJson = (project) => JSON.stringify({
|
|
9
10
|
repositoryKey: project.repositoryKey,
|
|
@@ -57,10 +58,14 @@ export function withLocalExecutionFacts(serverPrompt, maxBytes, options = {}) {
|
|
|
57
58
|
}
|
|
58
59
|
const remaining = maxBytes - requiredBytes - Math.max(0, reservedPromptBytes);
|
|
59
60
|
const factsBytes = Buffer.byteLength(localFacts, "utf8");
|
|
60
|
-
if (factsBytes > remaining)
|
|
61
|
+
if (factsBytes > remaining) {
|
|
62
|
+
if (!options.isolateLocalMemory && detectLocalMemoryStorageVersion(workspace.memory) === 2) {
|
|
63
|
+
throw new Error("local_memory_identity_budget_exceeded");
|
|
64
|
+
}
|
|
61
65
|
return `${serverPrompt}${collaborationPrompt}${mandatoryWorkspacePrompt}`;
|
|
66
|
+
}
|
|
62
67
|
const memoryHeading = "\n\n## Injected MEMORY.md (bounded local context)\n";
|
|
63
|
-
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"));
|
|
68
|
+
const memory = options.isolateLocalMemory ? "" : capMemoryForInject(workspace.memory, undefined, remaining - factsBytes - Buffer.byteLength(memoryHeading, "utf8"), options.memoryQuery);
|
|
64
69
|
const memorySection = memory === "" ? "" : memoryHeading + memory;
|
|
65
70
|
const workLogSection = workLog === "" ? "" : `\n\n## Injected work-log (bounded local context)\n${workLog}`;
|
|
66
71
|
const workLogBudget = remaining - factsBytes - Buffer.byteLength(memorySection, "utf8");
|