pwragent 1.3.9 → 1.3.10
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 +15 -1
- package/package.json +4 -4
- package/src/cli.js +14 -2
- package/src/daemon.js +1 -1
- package/src/federation-backend.js +19 -7
- package/src/runtime.js +5 -1
- package/src/version.js +36 -0
package/README.md
CHANGED
|
@@ -50,6 +50,19 @@ stop the daemon, install the selected version, and start it again. Do not remove
|
|
|
50
50
|
the PwrAgent root, Codex home or workspace during updates. There is no automatic
|
|
51
51
|
CLI updater or automatic systemd enablement.
|
|
52
52
|
|
|
53
|
+
`pwragent --version` prints the installed package version. A published package
|
|
54
|
+
also prints its release tag and source commit, for example
|
|
55
|
+
`pwragent 1.3.8 (v1.3.8, commit 0123…)`. `pwragent version` prints the same
|
|
56
|
+
fields as JSON. Neither needs a running daemon.
|
|
57
|
+
|
|
58
|
+
`pwragent status` reports the daemon's version as `running`, read when that
|
|
59
|
+
daemon started, and the package files as `installed`. `restartRequired` is
|
|
60
|
+
`true` when they differ, so the update has not taken effect yet. A daemon
|
|
61
|
+
started before version reporting has no `running` field and also reports
|
|
62
|
+
`true`. Run `status` from the same installation as the daemon, such as the
|
|
63
|
+
`/opt/pwragent` path in the systemd example; another installation compares its
|
|
64
|
+
own files.
|
|
65
|
+
|
|
53
66
|
## systemd installation example
|
|
54
67
|
|
|
55
68
|
On a Linux host with systemd, an administrator must first create the dedicated
|
|
@@ -91,7 +104,8 @@ sudo -u pwragent env PWRAGENT_HOME=/var/lib/pwragent PWRAGENT_PROFILE=outpost \
|
|
|
91
104
|
For a service update, finish or interrupt active work, run
|
|
92
105
|
`sudo systemctl stop pwragent-headless.service`, repeat the prefix installation
|
|
93
106
|
with the chosen new version (or `pwragent@latest`), then run
|
|
94
|
-
`sudo systemctl start pwragent-headless.service`.
|
|
107
|
+
`sudo systemctl start pwragent-headless.service`. Run the `status` command above
|
|
108
|
+
and check that `restartRequired` is `false`. Compare the new example unit
|
|
95
109
|
with the installed one before deliberately updating it; npm does not overwrite
|
|
96
110
|
`/etc/systemd/system`. Stopping during a turn can leave its outcome `unknown`.
|
|
97
111
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pwragent",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Experimental headless PwrAgent daemon and command-line client.",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@pwragent/federation": "workspace:*",
|
|
55
55
|
"@pwragent/shared": "workspace:*",
|
|
56
|
-
"esbuild": "0.28.
|
|
56
|
+
"esbuild": "0.28.2",
|
|
57
57
|
"@pwragent/agent-tools": "workspace:*"
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
"node-pty"
|
|
64
64
|
],
|
|
65
65
|
"pwragentRelease": {
|
|
66
|
-
"tag": "v1.3.
|
|
67
|
-
"commit": "
|
|
66
|
+
"tag": "v1.3.10",
|
|
67
|
+
"commit": "ff9b702144786e162b3e91c78d79791761944350"
|
|
68
68
|
}
|
|
69
69
|
}
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
6
6
|
import { call } from "./client.js";
|
|
7
7
|
import { serve } from "./daemon.js";
|
|
8
8
|
import { ACTIVE } from "./store.js";
|
|
9
|
+
import { formatVersion, installedVersion, packageVersion, restartRequired } from "./version.js";
|
|
9
10
|
|
|
10
11
|
const help = `PwrAgent Headless (experimental, Linux/macOS, Node >=22)
|
|
11
12
|
|
|
@@ -16,6 +17,7 @@ const help = `PwrAgent Headless (experimental, Linux/macOS, Node >=22)
|
|
|
16
17
|
pwragent project-add|project-remove --path /workspace/project
|
|
17
18
|
pwragent git-auth-configure --config-file /outpost/git-auth-settings.json
|
|
18
19
|
pwragent status | list
|
|
20
|
+
pwragent --version | version
|
|
19
21
|
pwragent submit --id JOB --prompt-file /task.txt [--sandbox read-only|workspace-write]
|
|
20
22
|
[--model MODEL] [--thread THREAD_ID] [--cwd /workspace/project]
|
|
21
23
|
pwragent show|read|interrupt|wait --id JOB
|
|
@@ -36,6 +38,7 @@ No provisioning, login, queue, automatic replay, or local messaging providers.
|
|
|
36
38
|
export async function main(args) {
|
|
37
39
|
const { values, positionals } = parseArgs({ args, allowPositionals: true, strict: true, options: {
|
|
38
40
|
help: { type: "boolean", short: "h" },
|
|
41
|
+
version: { type: "boolean" },
|
|
39
42
|
profile: { type: "string" },
|
|
40
43
|
workspace: { type: "string" },
|
|
41
44
|
"codex-home": { type: "string" },
|
|
@@ -52,7 +55,9 @@ export async function main(args) {
|
|
|
52
55
|
peer: { type: "string" },
|
|
53
56
|
name: { type: "string" }, remote: { type: "string" }, path: { type: "string" }, cwd: { type: "string" }, "config-file": { type: "string" },
|
|
54
57
|
} });
|
|
55
|
-
if (values.help
|
|
58
|
+
if (values.help) { console.log(help); return 0; }
|
|
59
|
+
if (values.version) { console.log(formatVersion(packageVersion())); return 0; }
|
|
60
|
+
if (!positionals.length) { console.log(help); return 0; }
|
|
56
61
|
if (positionals.length !== 1) throw new Error("Expected one command; see --help");
|
|
57
62
|
const command = positionals[0];
|
|
58
63
|
const allowed = {
|
|
@@ -60,7 +65,7 @@ export async function main(args) {
|
|
|
60
65
|
submit: ["id", "prompt-file", "thread", "model", "sandbox", "cwd"],
|
|
61
66
|
"project-list": [], "project-create": ["name", "label"], "project-clone": ["name", "remote", "label"],
|
|
62
67
|
"project-add": ["path", "label"], "project-remove": ["path"], "git-auth-configure": ["config-file"],
|
|
63
|
-
show: ["id"], read: ["id"], interrupt: ["id"], wait: ["id"], status: [], list: [],
|
|
68
|
+
show: ["id"], read: ["id"], interrupt: ["id"], wait: ["id"], status: [], list: [], version: [],
|
|
64
69
|
"federation-join": ["invite-file", "gateway", "label"],
|
|
65
70
|
"federation-invite": ["port", "label"],
|
|
66
71
|
"federation-status": [], "federation-leave": [], "federation-revoke": ["peer"],
|
|
@@ -69,6 +74,7 @@ export async function main(args) {
|
|
|
69
74
|
for (const option of Object.keys(values)) {
|
|
70
75
|
if (option !== "profile" && !allowed[command].includes(option)) throw new Error(`--${option} does not apply to ${command}`);
|
|
71
76
|
}
|
|
77
|
+
if (command === "version") { console.log(JSON.stringify(packageVersion())); return 0; }
|
|
72
78
|
const profile = values.profile || process.env.PWRAGENT_PROFILE || "default";
|
|
73
79
|
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(profile)) throw new Error("Invalid profile name");
|
|
74
80
|
const root = process.env.PWRAGENT_HOME || path.join(homedir(), ".pwragent");
|
|
@@ -118,6 +124,12 @@ export async function main(args) {
|
|
|
118
124
|
await delay(1000);
|
|
119
125
|
}
|
|
120
126
|
}
|
|
127
|
+
if (command === "status") {
|
|
128
|
+
const status = await call(directory, "status", {});
|
|
129
|
+
const installed = installedVersion();
|
|
130
|
+
console.log(JSON.stringify({ ...status, installed, restartRequired: restartRequired(status.running, installed) }));
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
121
133
|
console.log(JSON.stringify(await call(directory, command, { id: values.id })));
|
|
122
134
|
return 0;
|
|
123
135
|
}
|
package/src/daemon.js
CHANGED
|
@@ -116,7 +116,7 @@ export async function serve({ directory, workspace, home, command, profile }) {
|
|
|
116
116
|
});
|
|
117
117
|
if (shutdown) { server.close(); return await done; }
|
|
118
118
|
server.on("error", (error) => { void stop(error); });
|
|
119
|
-
console.log(JSON.stringify({ ready: true, socket: socketPath, pid: process.pid }));
|
|
119
|
+
console.log(JSON.stringify({ ready: true, socket: socketPath, pid: process.pid, running: runtime.running }));
|
|
120
120
|
return await done;
|
|
121
121
|
} catch (error) {
|
|
122
122
|
await stop(error);
|
|
@@ -23,6 +23,11 @@ export function ownerMethodCapabilities(methods, contract = FEDERATION_BACKEND_M
|
|
|
23
23
|
}
|
|
24
24
|
export const METHOD_CAPABILITIES = ownerMethodCapabilities(OWNER_METHODS);
|
|
25
25
|
|
|
26
|
+
// The placeholder title desktop gives a new thread. Viewers recognize this
|
|
27
|
+
// exact text as "no name yet".
|
|
28
|
+
export const PLACEHOLDER_THREAD_TITLE = "Untitled thread";
|
|
29
|
+
const LEGACY_PLACEHOLDER_THREAD_TITLE = "New outpost thread";
|
|
30
|
+
|
|
26
31
|
function mode(sandbox) { return sandbox === "workspace-write" ? "auto" : "default"; }
|
|
27
32
|
function sandboxFor(params, fallback = "read-only") {
|
|
28
33
|
if (params.executionMode && !["default", "auto", "full-access"].includes(params.executionMode)) throw new Error("Unsupported execution mode");
|
|
@@ -51,7 +56,9 @@ export class HeadlessBackend {
|
|
|
51
56
|
try {
|
|
52
57
|
const data = JSON.parse(readFileSync(this.file, "utf8"));
|
|
53
58
|
if (data.version !== 1 || !Array.isArray(data.threads)) throw new Error("Invalid headless thread registry");
|
|
54
|
-
this.threads = new Map(data.threads.map((thread) => [thread.id,
|
|
59
|
+
this.threads = new Map(data.threads.map((thread) => [thread.id,
|
|
60
|
+
thread.titleSource === "fallback" && thread.title === LEGACY_PLACEHOLDER_THREAD_TITLE
|
|
61
|
+
? { ...thread, title: PLACEHOLDER_THREAD_TITLE } : thread]));
|
|
55
62
|
} catch (error) {
|
|
56
63
|
if (error.code !== "ENOENT") throw error;
|
|
57
64
|
this.threads = new Map();
|
|
@@ -219,13 +226,18 @@ export class HeadlessBackend {
|
|
|
219
226
|
for (const key of ["seenAt", "seenUpdatedAt"]) {
|
|
220
227
|
if (params[key] !== undefined && (!Number.isFinite(params[key]) || params[key] < 0)) throw new Error(`Invalid ${key}`);
|
|
221
228
|
}
|
|
222
|
-
|
|
229
|
+
// An omitted watermark keeps the stored one (see MarkThreadSeenRequest).
|
|
230
|
+
// A thread never given one is pinned to the seenAt it was judged by,
|
|
231
|
+
// so recording a new seenAt cannot change its unread state.
|
|
232
|
+
thread.seenUpdatedAt = params.seenUpdatedAt ?? thread.seenUpdatedAt ?? thread.seenAt ?? 0;
|
|
223
233
|
thread.seenAt = params.seenAt ?? Date.now();
|
|
224
|
-
thread.seenUpdatedAt = seenUpdatedAt;
|
|
225
234
|
this.persist();
|
|
226
235
|
this.forgetSeenAttentionMember(thread.id);
|
|
227
|
-
this.
|
|
228
|
-
|
|
236
|
+
this.publish({ backend: "codex", notification: { method: "navigation/thread/seen",
|
|
237
|
+
params: { threadId: thread.id, ...(params.seenUpdatedAt !== undefined ? { seenUpdatedAt: params.seenUpdatedAt } : {}) } } }, "transcript");
|
|
238
|
+
this.invalidate(thread.id, "navigation/thread/seen");
|
|
239
|
+
return { backend: "codex", threadId: thread.id, seenAt: thread.seenAt,
|
|
240
|
+
...(params.seenUpdatedAt !== undefined ? { seenUpdatedAt: params.seenUpdatedAt } : {}) };
|
|
229
241
|
}
|
|
230
242
|
case "setThreadExecutionMode": {
|
|
231
243
|
const thread = this.owned(params.threadId);
|
|
@@ -315,11 +327,11 @@ export class HeadlessBackend {
|
|
|
315
327
|
const result = await this.runtime.codex.request("thread/start", { cwd, dynamicTools: this.runtime.messagingTools.buildDynamicToolSpecs(), approvalPolicy: "never", sandbox, ...(params.model ? { model: params.model } : {}), ephemeral: false });
|
|
316
328
|
if (!result?.thread?.id || typeof result.thread.id !== "string" || result.thread.id.length > 256) throw new Error("Invalid thread/start response");
|
|
317
329
|
this.runtime.loadedThreads.add(result.thread.id);
|
|
318
|
-
const thread = { id: result.thread.id, messagingFileTools: true, title:
|
|
330
|
+
const thread = { id: result.thread.id, messagingFileTools: true, title: PLACEHOLDER_THREAD_TITLE, titleSource: "fallback", createdAt: Date.now(), cwd, sandbox,
|
|
319
331
|
model: params.model, reasoningEffort: params.reasoningEffort, serviceTier: params.serviceTier };
|
|
320
332
|
this.threads.set(thread.id, thread);
|
|
321
333
|
this.persist();
|
|
322
|
-
this.invalidate(thread.id);
|
|
334
|
+
this.invalidate(thread.id, "thread/started");
|
|
323
335
|
return { backend: "codex", threadId: thread.id, executionMode: mode(sandbox) };
|
|
324
336
|
} finally { this.creating = false; }
|
|
325
337
|
}
|
package/src/runtime.js
CHANGED
|
@@ -3,12 +3,16 @@ import path from "node:path";
|
|
|
3
3
|
import { readFileSync, readdirSync } from "node:fs";
|
|
4
4
|
import { AgentToolRouter, buildPwrAgentMessagingToolDefinitions, readPwrAgentMessagingDynamicToolCall, resolveMessagingOutboundFile, validateMessagingFileMetadata, FILE_PUSH_MAX_BYTES } from "../dist/federation-core.js";
|
|
5
5
|
import { Codex } from "./codex.js";
|
|
6
|
+
import { installedVersion } from "./version.js";
|
|
6
7
|
import { ACTIVE, atomicJson, digest, privateDirectory, validId } from "./store.js";
|
|
7
8
|
|
|
8
9
|
export class Runtime {
|
|
9
10
|
constructor({ store, workspace, home, command, onFailure }) {
|
|
10
11
|
this.store = store;
|
|
11
12
|
this.workspace = workspace;
|
|
13
|
+
// Snapshot at startup: upgrading the package files does not change the
|
|
14
|
+
// code this process is running.
|
|
15
|
+
this.running = installedVersion();
|
|
12
16
|
this.projects = new OutpostProjects(path.dirname(store.directory), workspace);
|
|
13
17
|
this.messagingTools = new AgentToolRouter(buildPwrAgentMessagingToolDefinitions((request) => this.messagingRequest(request), { nativeDynamicPdfImages: true }));
|
|
14
18
|
this.fileAccess = { privateStorageRoots: [home, process.env.PWRAGENT_HOME].filter(Boolean),
|
|
@@ -41,7 +45,7 @@ export class Runtime {
|
|
|
41
45
|
async handle(method, params = {}) {
|
|
42
46
|
if (this.stopping) throw new Error("Daemon is stopping");
|
|
43
47
|
switch (method) {
|
|
44
|
-
case "status": return { version: 1, ready: true, pid: process.pid, activeJob: this.active || null, workspace: this.workspace };
|
|
48
|
+
case "status": return { version: 1, ready: true, pid: process.pid, running: this.running, activeJob: this.active || null, workspace: this.workspace };
|
|
45
49
|
case "project-list": return this.projects.list();
|
|
46
50
|
case "project-create": case "project-clone": return this.projects.create(params);
|
|
47
51
|
case "project-add": return this.projects.add(params);
|
package/src/version.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// The release writes pwragentRelease: { tag, commit } into the published
|
|
4
|
+
// manifest. A checkout has none, so only string fields are reported.
|
|
5
|
+
export function versionInfo(pkg) {
|
|
6
|
+
const source = pkg.pwragentRelease;
|
|
7
|
+
const release = {};
|
|
8
|
+
if (typeof source?.tag === "string") release.tag = source.tag;
|
|
9
|
+
if (typeof source?.commit === "string") release.commit = source.commit;
|
|
10
|
+
return { name: pkg.name, version: pkg.version, ...(Object.keys(release).length ? { release } : {}) };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Read on each call from this package's own manifest, so the CLI reports the
|
|
14
|
+
// files currently installed. The daemon reads it once at startup instead.
|
|
15
|
+
export function packageVersion() {
|
|
16
|
+
return versionInfo(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The version fields that status compares, without the package name.
|
|
20
|
+
export function installedVersion() {
|
|
21
|
+
const { name: _name, ...fields } = packageVersion();
|
|
22
|
+
return fields;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function formatVersion({ name, version, release }) {
|
|
26
|
+
const details = [release?.tag, release?.commit && `commit ${release.commit}`].filter(Boolean);
|
|
27
|
+
return details.length ? `${name} ${version} (${details.join(", ")})` : `${name} ${version}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// A daemon that predates version reporting returns no running version. It is
|
|
31
|
+
// older than this installation, so it needs a restart too.
|
|
32
|
+
export function restartRequired(running, installed) {
|
|
33
|
+
return !running
|
|
34
|
+
|| running.version !== installed.version
|
|
35
|
+
|| running.release?.commit !== installed.release?.commit;
|
|
36
|
+
}
|