@timqi/pier 0.0.21 → 0.0.22
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/agent/config-sync.js +124 -92
- package/dist/db.js +5 -3
- package/dist/main.js +11 -4
- package/dist/service.js +17 -7
- package/dist/tasks/definitions.js +4 -0
- package/dist/tasks/tool.js +70 -13
- package/dist/web/instance.js +3 -3
- package/dist/web/public/assets/activity-Bv8fc5Wj.js +5 -0
- package/dist/web/public/assets/activity-Bv8fc5Wj.js.br +0 -0
- package/dist/web/public/assets/activity-Bv8fc5Wj.js.gz +0 -0
- package/dist/web/public/assets/boards-BRKjgEYX.js +1 -0
- package/dist/web/public/assets/boards-BRKjgEYX.js.br +0 -0
- package/dist/web/public/assets/boards-BRKjgEYX.js.gz +0 -0
- package/dist/web/public/assets/{code-C86xKk2k.js → code-BgAXjUH3.js} +1 -1
- package/dist/web/public/assets/{explorer-DTvsy5Rs.js → explorer-Cq_Ertso.js} +3 -3
- package/dist/web/public/assets/explorer-Cq_Ertso.js.br +0 -0
- package/dist/web/public/assets/explorer-Cq_Ertso.js.gz +0 -0
- package/dist/web/public/assets/{ghostty-web-CvSZragH.js → ghostty-web-CZHJ0YFi.js} +1 -1
- package/dist/web/public/assets/ghostty-web-CZHJ0YFi.js.br +0 -0
- package/dist/web/public/assets/ghostty-web-CZHJ0YFi.js.gz +0 -0
- package/dist/web/public/assets/index-BH7ir5I4.css +2 -0
- package/dist/web/public/assets/index-BH7ir5I4.css.br +0 -0
- package/dist/web/public/assets/index-BH7ir5I4.css.gz +0 -0
- package/dist/web/public/assets/index-BrBNzaid.js +79 -0
- package/dist/web/public/assets/index-BrBNzaid.js.br +0 -0
- package/dist/web/public/assets/index-BrBNzaid.js.gz +0 -0
- package/dist/web/public/assets/runs-CvdNDaw8.js +1 -0
- package/dist/web/public/assets/runs-CvdNDaw8.js.br +0 -0
- package/dist/web/public/assets/runs-CvdNDaw8.js.gz +0 -0
- package/dist/web/public/assets/settings-BaVwkUUD.js +6 -0
- package/dist/web/public/assets/settings-BaVwkUUD.js.br +0 -0
- package/dist/web/public/assets/settings-BaVwkUUD.js.gz +0 -0
- package/dist/web/public/assets/task-runs-B-eoldcU.js +3 -0
- package/dist/web/public/assets/task-runs-B-eoldcU.js.br +0 -0
- package/dist/web/public/assets/task-runs-B-eoldcU.js.gz +0 -0
- package/dist/web/public/assets/tasks-CT4LECPQ.js +4 -0
- package/dist/web/public/assets/tasks-CT4LECPQ.js.br +0 -0
- package/dist/web/public/assets/tasks-CT4LECPQ.js.gz +0 -0
- package/dist/web/public/assets/terminal-C0qFDFou.js +2 -0
- package/dist/web/public/assets/terminal-C0qFDFou.js.br +0 -0
- package/dist/web/public/assets/terminal-C0qFDFou.js.gz +0 -0
- package/dist/web/public/index.html +16 -16
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/docs/deploy.md +20 -8
- package/package.json +1 -1
- package/skills/pier-tasks/SKILL.md +20 -11
- package/dist/web/public/assets/activity-Cw0LRVNI.js +0 -5
- package/dist/web/public/assets/activity-Cw0LRVNI.js.br +0 -0
- package/dist/web/public/assets/activity-Cw0LRVNI.js.gz +0 -0
- package/dist/web/public/assets/boards-DMVbV84c.js +0 -1
- package/dist/web/public/assets/boards-DMVbV84c.js.br +0 -0
- package/dist/web/public/assets/boards-DMVbV84c.js.gz +0 -0
- package/dist/web/public/assets/explorer-DTvsy5Rs.js.br +0 -0
- package/dist/web/public/assets/explorer-DTvsy5Rs.js.gz +0 -0
- package/dist/web/public/assets/ghostty-web-CvSZragH.js.br +0 -0
- package/dist/web/public/assets/ghostty-web-CvSZragH.js.gz +0 -0
- package/dist/web/public/assets/index-DHxoYoeq.js +0 -79
- package/dist/web/public/assets/index-DHxoYoeq.js.br +0 -0
- package/dist/web/public/assets/index-DHxoYoeq.js.gz +0 -0
- package/dist/web/public/assets/index-DuZ2vERd.css +0 -2
- package/dist/web/public/assets/index-DuZ2vERd.css.br +0 -0
- package/dist/web/public/assets/index-DuZ2vERd.css.gz +0 -0
- package/dist/web/public/assets/runs-jY40JBP-.js +0 -1
- package/dist/web/public/assets/runs-jY40JBP-.js.br +0 -0
- package/dist/web/public/assets/runs-jY40JBP-.js.gz +0 -0
- package/dist/web/public/assets/settings-Dux7PIKF.js +0 -6
- package/dist/web/public/assets/settings-Dux7PIKF.js.br +0 -0
- package/dist/web/public/assets/settings-Dux7PIKF.js.gz +0 -0
- package/dist/web/public/assets/task-runs-DKvtBVxS.js +0 -3
- package/dist/web/public/assets/task-runs-DKvtBVxS.js.br +0 -0
- package/dist/web/public/assets/task-runs-DKvtBVxS.js.gz +0 -0
- package/dist/web/public/assets/tasks-CqfrOVIt.js +0 -4
- package/dist/web/public/assets/tasks-CqfrOVIt.js.br +0 -0
- package/dist/web/public/assets/tasks-CqfrOVIt.js.gz +0 -0
- package/dist/web/public/assets/terminal-DL_0yEy7.js +0 -2
- package/dist/web/public/assets/terminal-DL_0yEy7.js.br +0 -0
- package/dist/web/public/assets/terminal-DL_0yEy7.js.gz +0 -0
|
@@ -1,54 +1,98 @@
|
|
|
1
1
|
// Credential-blind projection of the portable agent configuration.
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/** Never leaves the instance, at any depth of a models.json provider: the
|
|
3
|
+
* credentials, and the endpoint they authenticate against — a sharing link is
|
|
4
|
+
* public. Every other field is metadata Pi's own schema already validates, so
|
|
5
|
+
* it travels as-is instead of through a field list chasing Pi's versions. */
|
|
6
|
+
const PRIVATE_KEYS = ["apiKey", "headers", "baseUrl"];
|
|
7
|
+
/** The catalog is the part of a provider sync owns outright: absent on the
|
|
8
|
+
* source means deleted here, while a field the source never states stays. */
|
|
9
|
+
const CATALOG_KEYS = ["models", "modelOverrides"];
|
|
10
|
+
/** Dropped strings shorter than this ("", "Bearer") collide with legitimate
|
|
11
|
+
* metadata; a credential or an endpoint does not. */
|
|
12
|
+
const SECRET_CHARS = 8;
|
|
13
|
+
const asRecord = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
4
14
|
function record(value, where) {
|
|
5
|
-
|
|
6
|
-
|
|
15
|
+
const plain = asRecord(value);
|
|
16
|
+
if (!plain || ![Object.prototype, null].includes(Object.getPrototypeOf(plain))) {
|
|
7
17
|
throw new Error(`${where} must be an object`);
|
|
8
18
|
}
|
|
9
|
-
return
|
|
19
|
+
return plain;
|
|
10
20
|
}
|
|
11
21
|
function only(value, keys, where) {
|
|
12
22
|
if (Object.keys(value).some((key) => !keys.includes(key))) {
|
|
13
23
|
throw new Error(`${where} contains unsupported fields`);
|
|
14
24
|
}
|
|
15
25
|
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
/** Every string in a JSON value, so one containment test covers the whole
|
|
27
|
+
* projection however deeply Pi nests a future field. */
|
|
28
|
+
function strings(value, found = []) {
|
|
29
|
+
const nested = asRecord(value);
|
|
30
|
+
if (typeof value === "string")
|
|
31
|
+
found.push(value);
|
|
32
|
+
else if (Array.isArray(value))
|
|
33
|
+
for (const entry of value)
|
|
34
|
+
strings(entry, found);
|
|
35
|
+
else if (nested)
|
|
36
|
+
for (const entry of Object.values(nested))
|
|
37
|
+
strings(entry, found);
|
|
38
|
+
return found;
|
|
39
|
+
}
|
|
40
|
+
/** Deep copy of JSON-only data; the private fields are dropped and handed to
|
|
41
|
+
* `onPrivate` — collected on the way out, refused on the way in. */
|
|
42
|
+
function project(value, where, onPrivate) {
|
|
43
|
+
if (value === null || ["string", "boolean", "number"].includes(typeof value))
|
|
44
|
+
return value;
|
|
45
|
+
if (Array.isArray(value))
|
|
46
|
+
return value.map((entry) => project(entry, where, onPrivate));
|
|
47
|
+
return Object.fromEntries(Object.entries(record(value, where)).flatMap(([key, entry]) => {
|
|
48
|
+
if (PRIVATE_KEYS.includes(key)) {
|
|
49
|
+
onPrivate(key, entry);
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
return [[key, project(entry, `${where} ${key}`, onPrivate)]];
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
55
|
+
/** The shape both sides must agree on: a provider id that cannot address
|
|
56
|
+
* anything but its own record, and a catalog addressable by model id. */
|
|
57
|
+
function portable(id, value, onPrivate) {
|
|
58
|
+
if (id.length > 100 || !/^[a-z0-9][a-z0-9._-]*$/.test(id) || ["constructor", "prototype"].includes(id)) {
|
|
59
|
+
throw new Error("invalid snapshot provider id");
|
|
29
60
|
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
throw new Error("
|
|
61
|
+
const provider = record(project(value, `provider ${id}`, onPrivate), `provider ${id}`);
|
|
62
|
+
const models = provider.models ?? [];
|
|
63
|
+
if (!Array.isArray(models))
|
|
64
|
+
throw new Error("models must be an array");
|
|
65
|
+
const ids = models.map((model) => record(model, "model").id);
|
|
66
|
+
if (ids.some((modelId) => typeof modelId !== "string" || !modelId.trim() || modelId !== modelId.trim())) {
|
|
67
|
+
throw new Error("model id must be a non-empty trimmed string");
|
|
34
68
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
69
|
+
if (new Set(ids).size !== ids.length)
|
|
70
|
+
throw new Error("duplicate model ids");
|
|
71
|
+
const overrides = provider.modelOverrides ?? {};
|
|
72
|
+
for (const [overrideId, override] of Object.entries(record(overrides, "modelOverrides"))) {
|
|
73
|
+
record(override, `modelOverrides ${overrideId}`);
|
|
39
74
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
75
|
+
return provider;
|
|
76
|
+
}
|
|
77
|
+
/** Project disk metadata before validation, then fail closed if anything the
|
|
78
|
+
* projection removed still appears in it: a deny list cannot foresee the field
|
|
79
|
+
* Pi adds next, but it can refuse to publish a value it just dropped. */
|
|
80
|
+
export function snapshotProviders(providers = {}) {
|
|
81
|
+
const dropped = [];
|
|
82
|
+
const shared = Object.fromEntries(Object.entries(providers).flatMap(([id, provider]) => {
|
|
83
|
+
const value = portable(id, provider, (_key, entry) => {
|
|
84
|
+
dropped.push(...strings(entry).filter((secret) => secret.length >= SECRET_CHARS));
|
|
85
|
+
});
|
|
86
|
+
return Object.keys(value).length ? [[id, value]] : [];
|
|
87
|
+
}));
|
|
88
|
+
const exported = strings(shared);
|
|
89
|
+
if (dropped.some((secret) => exported.some((value) => value.includes(secret)))) {
|
|
90
|
+
throw new Error("models.json repeats a credential or endpoint in a shared field; remove it before sharing");
|
|
48
91
|
}
|
|
49
|
-
return
|
|
92
|
+
return shared;
|
|
50
93
|
}
|
|
51
|
-
/** Strict import boundary:
|
|
94
|
+
/** Strict import boundary: a source that states a private field is refused,
|
|
95
|
+
* never trusted as a mask over the local one. */
|
|
52
96
|
export function normalizeAgentSnapshot(raw) {
|
|
53
97
|
const snapshot = record(raw, "agent snapshot");
|
|
54
98
|
only(snapshot, ["files", "providers"], "agent snapshot");
|
|
@@ -59,65 +103,53 @@ export function normalizeAgentSnapshot(raw) {
|
|
|
59
103
|
throw new Error(`${name} must be a string or null`);
|
|
60
104
|
}
|
|
61
105
|
}
|
|
62
|
-
const providers = Object.fromEntries(Object.entries(record(snapshot.providers, "snapshot providers"))
|
|
63
|
-
|
|
64
|
-
throw new Error(
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const models = provider.models.map(modelDefinition);
|
|
71
|
-
if (new Set(models.map((model) => model.id)).size !== models.length)
|
|
72
|
-
throw new Error("duplicate model ids");
|
|
73
|
-
return [id, { models }];
|
|
74
|
-
}));
|
|
75
|
-
return { files: { "SYSTEM.md": files["SYSTEM.md"], "AGENTS.md": files["AGENTS.md"] }, providers };
|
|
106
|
+
const providers = Object.fromEntries(Object.entries(record(snapshot.providers, "snapshot providers"))
|
|
107
|
+
.map(([id, provider]) => [id, portable(id, provider, (key) => {
|
|
108
|
+
throw new Error(`snapshot provider must not carry ${key}`);
|
|
109
|
+
})]));
|
|
110
|
+
return {
|
|
111
|
+
files: { "SYSTEM.md": files["SYSTEM.md"], "AGENTS.md": files["AGENTS.md"] },
|
|
112
|
+
providers,
|
|
113
|
+
};
|
|
76
114
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const models = provider.models.map((value) => {
|
|
85
|
-
const source = record(value, "models.json model");
|
|
86
|
-
const model = Object.fromEntries(MODEL_KEYS.flatMap((key) => source[key] === undefined ? [] : [[key, source[key]]]));
|
|
87
|
-
if (source.cost !== undefined) {
|
|
88
|
-
const cost = record(source.cost, "models.json model cost");
|
|
89
|
-
model.cost = Object.fromEntries(COST_KEYS.flatMap((key) => cost[key] === undefined ? [] : [[key, cost[key]]]));
|
|
90
|
-
}
|
|
91
|
-
return modelDefinition(model);
|
|
92
|
-
});
|
|
93
|
-
return [[id, { models }]];
|
|
94
|
-
}));
|
|
115
|
+
/** A model or override arrives whole; only the local private fields survive it. */
|
|
116
|
+
function withLocalPrivate(incoming, local) {
|
|
117
|
+
const merged = { ...incoming };
|
|
118
|
+
for (const key of PRIVATE_KEYS)
|
|
119
|
+
if (local && Object.hasOwn(local, key))
|
|
120
|
+
merged[key] = local[key];
|
|
121
|
+
return merged;
|
|
95
122
|
}
|
|
96
|
-
|
|
123
|
+
const modelsById = (models) => new Map((Array.isArray(models) ? models : []).flatMap((model) => {
|
|
124
|
+
const value = asRecord(model);
|
|
125
|
+
return value ? [[value.id, value]] : [];
|
|
126
|
+
}));
|
|
127
|
+
/** The source sets what it states and replaces the catalog; credentials and
|
|
128
|
+
* endpoints stay local, matched by provider, model id and override id. */
|
|
97
129
|
export function mergeSnapshotProviders(local = {}, incoming) {
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
if (!Object.hasOwn(incoming, id)) {
|
|
101
|
-
delete provider.models;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
for (const [id, { models }] of Object.entries(incoming)) {
|
|
105
|
-
if (!Object.hasOwn(local, id)) {
|
|
130
|
+
for (const id of Object.keys(incoming)) {
|
|
131
|
+
if (!Object.hasOwn(local, id))
|
|
106
132
|
throw new Error(`provider ${id} must be configured locally before importing models`);
|
|
107
|
-
}
|
|
108
|
-
const provider = providers[id];
|
|
109
|
-
if (provider.models !== undefined && !Array.isArray(provider.models))
|
|
110
|
-
throw new Error("models.json models must be an array");
|
|
111
|
-
const existing = new Map((provider.models ?? []).map((value) => {
|
|
112
|
-
const model = record(value, "models.json model");
|
|
113
|
-
return [model.id, model];
|
|
114
|
-
}));
|
|
115
|
-
provider.models = models.map((model) => {
|
|
116
|
-
const retained = { ...existing.get(model.id) };
|
|
117
|
-
for (const key of MODEL_KEYS)
|
|
118
|
-
delete retained[key];
|
|
119
|
-
return { ...retained, ...model };
|
|
120
|
-
});
|
|
121
133
|
}
|
|
122
|
-
return
|
|
134
|
+
return Object.fromEntries(Object.entries(local).map(([id, provider]) => {
|
|
135
|
+
const current = record(provider, "models.json provider");
|
|
136
|
+
const source = incoming[id];
|
|
137
|
+
if (!source)
|
|
138
|
+
return [id, Object.fromEntries(Object.entries(current).filter(([key]) => !CATALOG_KEYS.includes(key)))];
|
|
139
|
+
// The catalog shapes are the ones normalizeAgentSnapshot already proved.
|
|
140
|
+
const merged = { ...current, ...structuredClone(source) };
|
|
141
|
+
for (const key of CATALOG_KEYS)
|
|
142
|
+
if (!Object.hasOwn(source, key))
|
|
143
|
+
delete merged[key];
|
|
144
|
+
if (merged.models !== undefined) {
|
|
145
|
+
const previous = modelsById(current.models);
|
|
146
|
+
merged.models = merged.models.map((model) => withLocalPrivate(model, previous.get(model.id)));
|
|
147
|
+
}
|
|
148
|
+
if (merged.modelOverrides !== undefined) {
|
|
149
|
+
const previous = asRecord(current.modelOverrides) ?? {};
|
|
150
|
+
merged.modelOverrides = Object.fromEntries(Object.entries(merged.modelOverrides)
|
|
151
|
+
.map(([key, override]) => [key, withLocalPrivate(override, asRecord(previous[key]))]));
|
|
152
|
+
}
|
|
153
|
+
return [id, merged];
|
|
154
|
+
}));
|
|
123
155
|
}
|
package/dist/db.js
CHANGED
|
@@ -361,9 +361,11 @@ let shared;
|
|
|
361
361
|
* defaults to it; a test passes `openDb(":memory:")` instead.
|
|
362
362
|
*/
|
|
363
363
|
export const pierDb = () => (shared ??= openDb(PIER_DB));
|
|
364
|
-
/** A release-level restore point, taken
|
|
365
|
-
*
|
|
366
|
-
*
|
|
364
|
+
/** A release-level restore point, taken for every release even when it has no
|
|
365
|
+
* schema migration. The previous complete copies stay put if writing this one
|
|
366
|
+
* fails, and the service may be running while it is written: `copyDatabase`
|
|
367
|
+
* reads through a read-only connection, so what it writes is one consistent
|
|
368
|
+
* snapshot of a live database rather than a torn `cp`.
|
|
367
369
|
*
|
|
368
370
|
* `version` is the Pier that produced this database, not the one being
|
|
369
371
|
* installed: the updater runs this from the tree it is about to replace, and
|
package/dist/main.js
CHANGED
|
@@ -205,8 +205,9 @@ await configurationSync.reconcile();
|
|
|
205
205
|
tasks.start();
|
|
206
206
|
readyForConfigReload = true;
|
|
207
207
|
void secrets.unlock().then(startChannels, (err) => log.error("secrets locked — channels not started; unlock from Console → Settings → Security, or repair master.key", err));
|
|
208
|
-
// Replacing Pier is systemd's job, not this process's: the oneshot unit
|
|
209
|
-
//
|
|
208
|
+
// Replacing Pier is systemd's job, not this process's: the oneshot unit
|
|
209
|
+
// snapshots the database, installs, then stops the service and starts it again
|
|
210
|
+
// on the new version — in that order, so only the last two are downtime. Without
|
|
210
211
|
// that unit there is nothing to hand the work to, and the Console says so
|
|
211
212
|
// instead of offering a button that cannot work.
|
|
212
213
|
const updates = new UpdateCheck();
|
|
@@ -247,8 +248,14 @@ const takeWorkAgain = (why) => {
|
|
|
247
248
|
};
|
|
248
249
|
/** How long the handover has to actually stop us. `systemctl start --no-block`
|
|
249
250
|
* returns when the job is *queued*, so "started" is not proof of anything;
|
|
250
|
-
* the real outcome is a SIGTERM
|
|
251
|
-
|
|
251
|
+
* the real outcome is a SIGTERM — but only after the updater has snapshotted
|
|
252
|
+
* the database and installed the new version, which is a registry download on
|
|
253
|
+
* someone else's network — ten seconds on a good day, minutes on a bad one.
|
|
254
|
+
* So this waits far longer than the stop itself needs, because reopening the
|
|
255
|
+
* gate mid-install would take work we are about to be SIGTERM'd out of, and
|
|
256
|
+
* still short enough that a handover which never happens does not refuse
|
|
257
|
+
* messages all afternoon. */
|
|
258
|
+
const HANDOVER_GRACE_MS = 5 * 60_000;
|
|
252
259
|
const handOverToUpdater = async () => {
|
|
253
260
|
// One handover at a time, and never on top of a restart: the Console button,
|
|
254
261
|
// the auto-update tick and SIGUSR2 would otherwise drain the same Pier
|
package/dist/service.js
CHANGED
|
@@ -104,8 +104,14 @@ function legacyOptions(home) {
|
|
|
104
104
|
...(pierHome ? { pierHome } : {}),
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
/** A separate cgroup
|
|
108
|
-
*
|
|
107
|
+
/** A separate cgroup snapshots the database, updates the exact npm
|
|
108
|
+
* installation recorded at install time, and only then restarts Pier.
|
|
109
|
+
*
|
|
110
|
+
* Both slow steps run while the service is still up, so the downtime is one
|
|
111
|
+
* stop and one start rather than the ~10s an install takes. The snapshot is
|
|
112
|
+
* consistent on a live database — `VACUUM INTO` off a read-only connection
|
|
113
|
+
* (db.ts) — and it still runs from the tree npm is about to replace, so the
|
|
114
|
+
* copy carries the version whose schema it pairs with. */
|
|
109
115
|
export function renderUpdateUnit(options) {
|
|
110
116
|
const { execPath, npmPath, entry, pierHome, shellPath } = options;
|
|
111
117
|
const cli = join(dirname(entry), "cli.js");
|
|
@@ -120,12 +126,16 @@ Type=oneshot
|
|
|
120
126
|
# resolves node from PATH, and systemd's minimal PATH has no fnm/nvm node —
|
|
121
127
|
# the install then dies with "node: not found" with the tree half written.
|
|
122
128
|
${environment("PATH", pathEnv(execPath, shellPath))}
|
|
123
|
-
${pierHome ? `${environment("PIER_HOME", pierHome)}\n` : ""}ExecStart
|
|
124
|
-
ExecStart=${quote(execPath, true)} ${quote(cli, true)} backup
|
|
129
|
+
${pierHome ? `${environment("PIER_HOME", pierHome)}\n` : ""}ExecStart=${quote(execPath, true)} ${quote(cli, true)} backup
|
|
125
130
|
# npm runs under the recorded node: its shebang needs a node on PATH too.
|
|
126
131
|
ExecStart=${quote(execPath, true)} ${quote(npmPath, true)} install -g @timqi/pier@latest
|
|
127
|
-
#
|
|
128
|
-
#
|
|
132
|
+
# Last, because everything above it is downtime the running Pier does not owe
|
|
133
|
+
# anyone. A failed backup or install never gets here: the service stays up on
|
|
134
|
+
# the code it already loaded.
|
|
135
|
+
ExecStart=systemctl --user stop ${UNIT_NAME}
|
|
136
|
+
# ExecStopPost runs on success and failure. After a failed step above, Pier was
|
|
137
|
+
# never stopped and this is a no-op; it is here for the window that is still
|
|
138
|
+
# fatal — the unit dying between that stop and the start of the new version.
|
|
129
139
|
ExecStopPost=systemctl --user start ${UNIT_NAME}
|
|
130
140
|
`;
|
|
131
141
|
}
|
|
@@ -300,7 +310,7 @@ export function startUpdate(options) {
|
|
|
300
310
|
if (!run(["systemctl", "--user", "start", "--no-block", UPDATE_UNIT_NAME]))
|
|
301
311
|
return "failed";
|
|
302
312
|
say(`updating in the background — follow it with: journalctl --user -u ${UPDATE_UNIT_NAME} -f`);
|
|
303
|
-
say(`Pier
|
|
313
|
+
say(`Pier snapshots the database into db/backups/ and installs while still up, then stops and starts on the new version.`);
|
|
304
314
|
return "started";
|
|
305
315
|
}
|
|
306
316
|
export function uninstall(home = homedir(), say = console.log, exec) {
|
|
@@ -247,6 +247,10 @@ export class TaskDefinitions {
|
|
|
247
247
|
return this.router.stateOf(sessionId) !== undefined ||
|
|
248
248
|
(await this.factory.find(sessionId)) !== undefined;
|
|
249
249
|
}
|
|
250
|
+
/** A caller's own directory — what a relative or omitted cwd resolves against. */
|
|
251
|
+
async sessionCwd(sessionId) {
|
|
252
|
+
return (await this.factory.find(sessionId))?.cwd;
|
|
253
|
+
}
|
|
250
254
|
/** The guard `ownerOf` exists for, on the three ways a definition changes. */
|
|
251
255
|
assertOwner(task, by, what) {
|
|
252
256
|
const owner = ownerOf(task);
|
package/dist/tasks/tool.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAbsolute, resolve } from "node:path";
|
|
1
2
|
import { Type } from "typebox";
|
|
2
3
|
import { TaskDefinitions, record, requiredString } from "./definitions.js";
|
|
3
4
|
import { TaskMessenger } from "./messages.js";
|
|
@@ -57,10 +58,14 @@ const summarizeGroup = (group, members, messages) => defined({
|
|
|
57
58
|
winnerRunId: group.winnerRunId,
|
|
58
59
|
members: members.map((run) => trimResult(summarize(run, messages.openDecisionId(run.id)))),
|
|
59
60
|
});
|
|
61
|
+
const LaunchSchema = Type.Object({
|
|
62
|
+
model: Type.Optional(Type.Object({ provider: Type.String(), id: Type.String() })),
|
|
63
|
+
thinking: Type.Optional(Type.String()),
|
|
64
|
+
});
|
|
60
65
|
// Model-facing draft shape. Guidance only: runtime truth stays in parseDraft,
|
|
61
66
|
// so schema drift can never loosen boundary validation.
|
|
62
67
|
const DraftSchema = Type.Object({
|
|
63
|
-
name: Type.String(),
|
|
68
|
+
name: Type.Optional(Type.String({ description: "Defaults to the prompt's first line." })),
|
|
64
69
|
description: Type.Optional(Type.String()),
|
|
65
70
|
trigger: Type.Optional(Type.Union([
|
|
66
71
|
Type.Object({ type: Type.Literal("manual") }),
|
|
@@ -77,14 +82,11 @@ const DraftSchema = Type.Object({
|
|
|
77
82
|
Type.Object({
|
|
78
83
|
type: Type.Literal("agent"),
|
|
79
84
|
session: Type.Union([
|
|
80
|
-
Type.Object({ mode: Type.Literal("fresh"), cwd: Type.String() }),
|
|
85
|
+
Type.Object({ mode: Type.Literal("fresh"), cwd: Type.Optional(Type.String({ description: "Absolute, or relative to your session's directory; omitted = your directory." })) }),
|
|
81
86
|
Type.Object({ mode: Type.Literal("reuse"), sessionId: Type.String() }),
|
|
82
87
|
]),
|
|
83
88
|
prompt: Type.String(),
|
|
84
|
-
launch: Type.Optional(
|
|
85
|
-
model: Type.Optional(Type.Object({ provider: Type.String(), id: Type.String() })),
|
|
86
|
-
thinking: Type.Optional(Type.String()),
|
|
87
|
-
})),
|
|
89
|
+
launch: Type.Optional(LaunchSchema),
|
|
88
90
|
}),
|
|
89
91
|
Type.Object({ type: Type.Literal("bash"), script: Type.String(), cwd: Type.String() }),
|
|
90
92
|
Type.Object({ type: Type.Literal("task"), taskId: Type.String() }),
|
|
@@ -101,7 +103,7 @@ export function taskToolSpec(execute) {
|
|
|
101
103
|
return {
|
|
102
104
|
name: "task",
|
|
103
105
|
label: "Pier Task",
|
|
104
|
-
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. Run executes a stored task by task_id, a one-shot subagent from
|
|
106
|
+
description: "Manage durable Pier tasks and subagents. Agent tasks run in a fresh session or a reused one. Run executes a stored task by task_id, a one-shot subagent from a prompt (shorthand: prompt + optional cwd/launch/name — cwd defaults to your own directory, relative paths resolve against it, name comes from the prompt) or from a full inline task draft, or a core-joined fan-out via tasks[] with join all|first. Get accepts run_id, group_id, or task_id for that task's recent runs. Every operation returns immediately: results, group joins, and decision replies arrive as callback messages. Use steer/follow_up/resume for child control and contact/reply for supervisor decisions. models lists the deployment's model menu (operator pins with intent notes, else the live catalog).",
|
|
105
107
|
parameters: Type.Object({
|
|
106
108
|
operation: strEnum("list", "create", "update", "run", "get", "cancel", "steer", "follow_up", "resume", "contact", "reply", "models"),
|
|
107
109
|
task_id: Type.Optional(Type.String()),
|
|
@@ -111,13 +113,19 @@ export function taskToolSpec(execute) {
|
|
|
111
113
|
message: Type.Optional(Type.String()),
|
|
112
114
|
reason: Type.Optional(strEnum("progress", "decision")),
|
|
113
115
|
session_mode: Type.Optional(strEnum("fresh")),
|
|
116
|
+
// The one-shot shorthand: a prompt is the whole delegation, and the
|
|
117
|
+
// fresh session in the caller's own directory is what it means.
|
|
118
|
+
prompt: Type.Optional(Type.String()),
|
|
119
|
+
cwd: Type.Optional(Type.String()),
|
|
120
|
+
launch: Type.Optional(LaunchSchema),
|
|
121
|
+
name: Type.Optional(Type.String()),
|
|
114
122
|
task: Type.Optional(DraftSchema),
|
|
115
123
|
// The same draft again, spelled out, cost more tokens in every session
|
|
116
124
|
// than the whole rest of this contract. One copy is the guidance; this
|
|
117
125
|
// one points at it, and `parseDraft` is what actually validates either.
|
|
118
126
|
tasks: Type.Optional(Type.Unsafe({
|
|
119
127
|
type: "array",
|
|
120
|
-
description: "2+ entries, each
|
|
128
|
+
description: "2+ entries, each a prompt string, {prompt, cwd?, launch?, name?}, a task draft shaped exactly like `task`, or {task_id}.",
|
|
121
129
|
items: { type: "object" },
|
|
122
130
|
})),
|
|
123
131
|
join: Type.Optional(strEnum("all", "first")),
|
|
@@ -140,12 +148,12 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
140
148
|
if (input.operation === "create") {
|
|
141
149
|
if (active)
|
|
142
150
|
throw new Error("subagents cannot create task definitions");
|
|
143
|
-
return definitions.create(input.task, `session:${callerSessionId}`);
|
|
151
|
+
return definitions.create(await expandDraft(definitions, input.task, callerSessionId), `session:${callerSessionId}`);
|
|
144
152
|
}
|
|
145
153
|
if (input.operation === "update") {
|
|
146
154
|
if (active)
|
|
147
155
|
throw new Error("subagents cannot update task definitions");
|
|
148
|
-
return definitions.update(requiredString(input.task_id, "task_id"), input.task);
|
|
156
|
+
return definitions.update(requiredString(input.task_id, "task_id"), await expandDraft(definitions, input.task, callerSessionId));
|
|
149
157
|
}
|
|
150
158
|
if (input.operation === "run") {
|
|
151
159
|
if (Array.isArray(input.tasks)) {
|
|
@@ -158,7 +166,7 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
158
166
|
throw new Error("tasks[] needs at least 2 entries; use task for a single run");
|
|
159
167
|
const resolved = [];
|
|
160
168
|
for (const rawEntry of input.tasks) {
|
|
161
|
-
const entry = record(rawEntry);
|
|
169
|
+
const entry = typeof rawEntry === "string" ? { prompt: rawEntry } : record(rawEntry);
|
|
162
170
|
if (!entry)
|
|
163
171
|
throw new Error("invalid tasks[] entry");
|
|
164
172
|
resolved.push(entry.task_id === undefined
|
|
@@ -168,7 +176,7 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
168
176
|
const { group, runs } = host.runGroup(resolved, input.join === "first" ? "first" : "all", callerSessionId, active?.id ?? null, input.callback === "none" ? null : callerSessionId);
|
|
169
177
|
return summarizeGroup(group, runs, messages);
|
|
170
178
|
}
|
|
171
|
-
const draft = input.task_id === undefined ?
|
|
179
|
+
const draft = input.task_id === undefined ? inlineDraft(input) : undefined;
|
|
172
180
|
const task = draft
|
|
173
181
|
? await resolveDraft(definitions, draft, active, callerSessionId)
|
|
174
182
|
: resolveStored(definitions, input.task_id, active);
|
|
@@ -245,9 +253,58 @@ export async function handleTaskTool(host, definitions, store, messages, raw, ca
|
|
|
245
253
|
}
|
|
246
254
|
throw new Error("unknown task operation");
|
|
247
255
|
}
|
|
256
|
+
/** A single run's draft: the top-level shorthand (`prompt` …) or `task`, never both. */
|
|
257
|
+
function inlineDraft(input) {
|
|
258
|
+
const { prompt, cwd, launch, name } = input;
|
|
259
|
+
if (prompt === undefined)
|
|
260
|
+
return record(input.task) ?? undefined;
|
|
261
|
+
if (input.task !== undefined)
|
|
262
|
+
throw new Error("use either prompt or task");
|
|
263
|
+
return { prompt, cwd, launch, name };
|
|
264
|
+
}
|
|
265
|
+
/** The prompt's first line, unmarked and cut short: a label for the Console,
|
|
266
|
+
* not an identifier — the run's id is what anything addresses. */
|
|
267
|
+
function nameFromPrompt(prompt) {
|
|
268
|
+
const line = prompt.split("\n")
|
|
269
|
+
.map((l) => l.replace(/^[\s#>*-]+/, "").replace(/[*_`]/g, "").replace(/\s+/g, " ").trim())
|
|
270
|
+
.find(Boolean) ?? "subagent";
|
|
271
|
+
return line.length > 60 ? `${line.slice(0, 59).trimEnd()}…` : line;
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* The shape a draft is validated in, from the shapes a caller may write it in.
|
|
275
|
+
* A `prompt` shorthand becomes a fresh Agent action; a fresh session's cwd
|
|
276
|
+
* resolves against the caller's own directory (and is that directory when
|
|
277
|
+
* omitted); a missing name is the prompt's first line. Everything the caller
|
|
278
|
+
* did spell out passes through untouched — parseDraft still judges it.
|
|
279
|
+
*/
|
|
280
|
+
async function expandDraft(definitions, raw, callerSessionId) {
|
|
281
|
+
let draft = record(raw);
|
|
282
|
+
if (!draft)
|
|
283
|
+
return raw;
|
|
284
|
+
if (typeof draft.prompt === "string") {
|
|
285
|
+
if (draft.action !== undefined)
|
|
286
|
+
throw new Error("use either prompt (shorthand) or action");
|
|
287
|
+
const { prompt, cwd, launch, name, ...rest } = draft;
|
|
288
|
+
draft = { ...rest, name, action: { type: "agent", session: { mode: "fresh", cwd }, prompt, launch } };
|
|
289
|
+
}
|
|
290
|
+
const action = record(draft.action);
|
|
291
|
+
const session = record(action?.session);
|
|
292
|
+
if (action?.type === "agent" && session?.mode === "fresh" && (session.cwd === undefined || (typeof session.cwd === "string" && !isAbsolute(session.cwd)))) {
|
|
293
|
+
const base = await definitions.sessionCwd(callerSessionId);
|
|
294
|
+
if (!base)
|
|
295
|
+
throw new Error(`cwd ${session.cwd === undefined ? "omitted" : `"${session.cwd}" is relative`} and the calling session has no working directory; give an absolute path`);
|
|
296
|
+
draft = { ...draft, action: { ...action, session: { ...session, cwd: resolve(base, session.cwd ?? ".") } } };
|
|
297
|
+
}
|
|
298
|
+
if (draft.name === undefined && typeof action?.prompt === "string")
|
|
299
|
+
draft = { ...draft, name: nameFromPrompt(action.prompt) };
|
|
300
|
+
return draft;
|
|
301
|
+
}
|
|
248
302
|
/** Inline one-shot subagent: persisted like any task (kind "subagent",
|
|
249
303
|
* filtered from default lists) so runs stay auditable and resumable. */
|
|
250
|
-
async function resolveDraft(definitions,
|
|
304
|
+
async function resolveDraft(definitions, raw, active, callerSessionId) {
|
|
305
|
+
const draft = record(await expandDraft(definitions, raw, callerSessionId));
|
|
306
|
+
if (!draft)
|
|
307
|
+
throw new Error("task definition required");
|
|
251
308
|
if (draft.trigger !== undefined && record(draft.trigger)?.type !== "manual") {
|
|
252
309
|
throw new Error("inline subagent tasks must use a manual trigger");
|
|
253
310
|
}
|
package/dist/web/instance.js
CHANGED
|
@@ -78,9 +78,9 @@ export function registerInstanceRoutes(app, deps) {
|
|
|
78
78
|
problem: updater?.problem() ?? null,
|
|
79
79
|
}));
|
|
80
80
|
// Applying. Nothing is installed here: the work is handed to the service
|
|
81
|
-
// manager's own oneshot unit, which
|
|
82
|
-
//
|
|
83
|
-
// killed by the very restart it is performing.
|
|
81
|
+
// manager's own oneshot unit, which backs the database up and installs while
|
|
82
|
+
// Pier still runs, then stops it and starts the new version — an npm child of
|
|
83
|
+
// this process would be killed by the very restart it is performing.
|
|
84
84
|
app.post("/api/update", async (c) => {
|
|
85
85
|
if (!updater) {
|
|
86
86
|
return c.json({ error: "no service manager owns this Pier — update it with: pier update" }, 409);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import{B as e,C as t,H as n,I as r,L as i,N as a,k as o,m as s,q as c,v as l,z as u}from"./index-BrBNzaid.js";var d=(e,t={})=>{let n=document.createElementNS(`http://www.w3.org/2000/svg`,e);for(let[e,r]of Object.entries(t))n.setAttribute(e,r);return n},f=e=>e===null?`-`:r(Date.now()-e),p={light:{edge:`#a3a3a3`,callback:`#0891b2`,message:`#d97706`,scheduler:{fill:`#f5f3ff`,stroke:`#a78bfa`,text:`#5b21b6`},console:{fill:`#eff6ff`,stroke:`#93c5fd`,text:`#1d4ed8`},task:{fill:`#fffbeb`,stroke:`#fbbf24`,text:`#92400e`},process:{fill:`#f5f5f5`,stroke:`#a3a3a3`,text:`#525252`,dash:`4 3`},streaming:{fill:`#ffffff`,stroke:`#10b981`,text:`#262626`},idle:{fill:`#fafafa`,stroke:`#d4d4d4`,text:`#737373`}},dark:{edge:`#7a7a7a`,callback:`#3fc3dd`,message:`#e0a13a`,scheduler:{fill:`#2b2440`,stroke:`#7d63c9`,text:`#c9b8f5`},console:{fill:`#1f2a3d`,stroke:`#4a7fbf`,text:`#a8c8f0`},task:{fill:`#3a2f1c`,stroke:`#b8862a`,text:`#f0d49a`},process:{fill:`#262626`,stroke:`#5c5c5c`,text:`#a3a3a3`,dash:`4 3`},streaming:{fill:`#22302a`,stroke:`#10b981`,text:`#e5e5e5`},idle:{fill:`#212121`,stroke:`#4a4a4a`,text:`#9a9a9a`}}},m=()=>p[document.documentElement.dataset.theme===`dark`?`dark`:`light`];function h(r,p,h){let g=`sessions`,_=`active`,v={sessions:[],runs:[],messages:[]},y=``,b=e(async()=>{let e=_,t=await n(`/api/activity?scope=${e}`,`Failed to load activity`);if(!t.ok){y=``,r.replaceChildren(i(`p`,`p-4 text-[13px] text-red-600`,t.error));return}if(e!==_)return;let a=JSON.stringify(t.value);if(`${e}:${a}`===y)return;y=`${e}:${a}`;let o=t.value;v={...o,sessions:o.sessions.map(e=>({...e,title:c(e.title)}))},x()});function x(){let e=o(t([[`Sessions`,`sessions`],[`Relationships`,`dependencies`]],g,e=>{g=e,x()}),i(`span`,`ml-auto text-[11.5px] text-neutral-400`,`${v.sessions.length} sessions · ${v.runs.length} runs`),t([[`Current`,`active`],[`Last 24h`,`recent`]],_,e=>{_=e,x(),b()})),n=i(`div`,`min-h-0 flex-1 overflow-auto`);g===`sessions`?S(n):w(n),r.replaceChildren(e,n)}function S(e){let t=document.createElement(`table`);t.className=`w-full table-fixed text-left text-[12.5px]`,t.innerHTML=`<thead class="sticky top-0 bg-neutral-50 text-[10.5px] uppercase tracking-wide text-neutral-400 shadow-[inset_0_-1px_0_var(--color-neutral-200)]"><tr>
|
|
2
|
+
<th class="w-[50%] px-4 py-2 font-semibold md:w-[34%]">Session</th>
|
|
3
|
+
<th class="hidden w-[38%] px-2 py-2 font-semibold md:table-cell">Project</th>
|
|
4
|
+
<th class="px-2 py-2 font-semibold md:w-[14%]">State</th>
|
|
5
|
+
<th class="px-2 py-2 font-semibold">In state</th></tr></thead>`;let n=document.createElement(`tbody`);for(let e of v.sessions){let t=document.createElement(`tr`);t.className=`cursor-pointer border-b border-neutral-100 transition-colors hover:bg-neutral-50`,t.onclick=()=>p(e.id);let r=i(`button`,`block w-full cursor-pointer truncate text-left font-medium text-neutral-800`,e.title??u(e.cwd));r.setAttribute(`type`,`button`),r.title=`${e.id}\n${e.cwd}`,r.onclick=t=>{t.stopPropagation(),p(e.id)},t.append(i(`td`,`px-4 py-2.5`,r,i(`div`,`truncate font-mono text-[11px] text-neutral-400 md:hidden`,e.cwd||`-`)),i(`td`,`hidden truncate px-2 py-2.5 font-mono text-[11.5px] text-neutral-500 md:table-cell`,e.cwd||`-`),i(`td`,`px-2 py-2.5`,C(e.state)),i(`td`,`px-2 py-2.5 font-mono text-[11.5px] text-neutral-400`,f(e.stateSince))),n.append(t)}t.append(n),e.append(t),v.sessions.length||e.append(i(`div`,`p-4`,l(_===`active`?`No active sessions.`:`No sessions in the last 24 hours.`)))}function C(e){return e===`streaming`?s(e,`bg-emerald-50 text-emerald-700 ring-emerald-200`,`animate-pulse bg-emerald-500`):s(e,`bg-neutral-100 text-neutral-600 ring-neutral-200`,`bg-neutral-300`)}function w(e){let t=new Map,n=[],r=(e,n,r,i)=>{t.has(e)||t.set(e,{id:e,label:n,kind:r,state:i})};for(let e of v.sessions)r(e.id,e.title??e.id.slice(0,10),`session`,e.state);for(let e of v.runs){let t=e.invokedBySessionId??`scheduler`,i=e.targetSessionId??(e.context.definition.action.type===`bash`?`process:${e.id}`:`task:${e.id}`);r(t,t===`scheduler`?`Scheduler`:t.slice(0,10),t===`scheduler`?`scheduler`:`session`),r(i,e.targetSessionId?e.targetSessionId.slice(0,10):e.context.definition.action.type===`bash`?`Bash process`:`Task`,e.targetSessionId?`session`:e.context.definition.action.type===`bash`?`process`:`task`),n.push({from:t,to:i,run:e,kind:`invocation`}),e.callbackSessionId&&(r(e.callbackSessionId,e.callbackSessionId.slice(0,10),`session`),n.push({from:i,to:e.callbackSessionId,run:e,kind:`callback`}))}for(let e of v.messages){if(!e.fromSessionId||!e.toSessionId||e.fromSessionId===e.toSessionId)continue;let t=v.runs.find(t=>t.id===e.runId);if(t){for(let t of[e.fromSessionId,e.toSessionId])r(t,t===`console`?`Console`:t.slice(0,10),t===`console`?`console`:`session`);n.push({from:e.fromSessionId,to:e.toSessionId,run:t,kind:`message`})}}if(!t.size){e.append(i(`div`,`p-4`,l(_===`active`?`No active dependencies.`:`No dependencies in the last 24 hours.`)));return}let a=[...t.values()],o=new Set(n.map(e=>e.to)),s=new Map,c=a.filter(e=>!o.has(e.id)).map(e=>e.id);c.length||c.push(a[0].id);for(let e of c)s.set(e,0);for(;c.length;){let e=c.shift();for(let t of n)t.from!==e||s.has(t.to)||(s.set(t.to,s.get(e)+1),c.push(t.to))}for(let e of a)s.has(e.id)||s.set(e.id,0);let u=new Map;for(let e of a){let t=s.get(e.id);u.set(t,[...u.get(t)??[],e])}let f=Math.max(...u.keys())+1,g=Math.max(...[...u.values()].map(e=>e.length)),y=n.some(e=>e.from!==e.to&&s.get(e.from)===s.get(e.to))?40:0,b=56+f*180+(f-1)*110+y,x=56+g*46+(g-1)*30,S=new Map;for(let[e,t]of u)t.forEach((t,n)=>S.set(t.id,{x:28+e*290+90,y:28+n*76+23}));let C=d(`svg`,{width:String(b),height:String(x),viewBox:`0 0 ${b} ${x}`,class:`block`}),w=d(`defs`),T=m();for(let[e,t]of[[`activity-arrow`,T.edge],[`activity-arrow-cb`,T.callback],[`activity-arrow-msg`,T.message]]){let n=d(`marker`,{id:e,viewBox:`0 0 10 10`,refX:`9`,refY:`5`,markerWidth:`6`,markerHeight:`6`,orient:`auto-start-reverse`});n.append(d(`path`,{d:`M 0 0 L 10 5 L 0 10 z`,fill:t})),w.append(n)}C.append(w);let E=new Set;for(let e of n){let t=`${e.from}→${e.to}:${e.kind}`;if(E.has(t))continue;E.add(t);let n=S.get(e.from),r=S.get(e.to),i=d(`path`,{d:n.x===r.x?`M ${n.x+90} ${n.y} C ${n.x+90+46} ${n.y}, ${r.x+90+46} ${r.y}, ${r.x+90} ${r.y}`:(()=>{let e=r.x>n.x,t=n.x+(e?90:-90),i=r.x+(e?-90:90),a=Math.max(36,Math.abs(i-t)*.45)*(e?1:-1);return`M ${t} ${n.y} C ${t+a} ${n.y}, ${i-a} ${r.y}, ${i} ${r.y}`})(),fill:`none`,stroke:e.kind===`callback`?T.callback:e.kind===`message`?T.message:T.edge,"stroke-width":`1.5`,"marker-end":e.kind===`callback`?`url(#activity-arrow-cb)`:e.kind===`message`?`url(#activity-arrow-msg)`:`url(#activity-arrow)`});e.kind===`callback`&&i.setAttribute(`stroke-dasharray`,`6 5`),e.kind===`message`&&i.setAttribute(`stroke-dasharray`,`2 5`),i.classList.add(`cursor-pointer`),i.onclick=()=>h(e.run.id),C.append(i)}let D=/[\u1100-\u11FF\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6]/,O=(e,t)=>{let n=0;for(let r=0;r<e.length;r++)if(n+=D.test(e[r])?2:1,n>t)return`${e.slice(0,r)}…`;return e};for(let e of a){let t=S.get(e.id),n=e.kind===`session`?e.state===`streaming`?T.streaming:T.idle:T[e.kind],r=d(`g`,{transform:`translate(${t.x},${t.y})`});e.kind===`session`&&r.classList.add(`cursor-pointer`),r.onclick=()=>{e.kind===`session`&&p(e.id)};let i=d(`rect`,{x:`-90`,y:`-23`,width:`180`,height:`46`,rx:`8`,fill:n.fill,stroke:n.stroke,"stroke-width":e.kind===`session`&&e.state===`streaming`?`1.5`:`1`});n.dash&&i.setAttribute(`stroke-dasharray`,n.dash),r.append(i);let a=-78;if(e.kind===`session`){let t=d(`circle`,{cx:`-75`,cy:`0`,r:`3.5`,fill:n.stroke});if(e.state===`streaming`){let e=d(`animate`,{attributeName:`opacity`,values:`1;0.3;1`,dur:`1.6s`,repeatCount:`indefinite`});t.append(e)}r.append(t),a=-64}let o=d(`text`,{x:String(a),"text-anchor":`start`,"dominant-baseline":`middle`,"font-size":`11.5`,"font-weight":e.kind===`session`?`500`:`400`,fill:n.text});o.textContent=O(e.label,e.kind===`session`?22:25);let s=d(`title`);s.textContent=e.label===e.id?e.id:`${e.label}\n${e.id}`,r.append(o,s),C.append(r)}let k=e=>i(`span`,`inline-block h-2 w-2 rounded-full ${e}`),A=e=>i(`span`,`inline-block h-0 w-6 border-t-2 ${e}`);e.append(i(`div`,`w-max p-4`,C),i(`div`,`flex flex-wrap gap-x-5 gap-y-1.5 border-t border-neutral-200 bg-neutral-50/60 px-4 py-2 text-[11px] text-neutral-500`,i(`span`,`inline-flex items-center gap-2`,A(`border-solid border-neutral-400`),`task invocation`),i(`span`,`inline-flex items-center gap-2 text-cyan-700`,A(`border-dashed border-cyan-500`),`callback`),i(`span`,`inline-flex items-center gap-2 text-amber-700`,A(`border-dotted border-amber-500`),`supervisor / control`),i(`span`,`inline-flex items-center gap-1.5`,k(`bg-emerald-500`),`streaming`),i(`span`,`inline-flex items-center gap-1.5`,k(`bg-neutral-300`),`idle`)))}let T=a(r,e=>{e===`dependencies`?g=`dependencies`:e===`sessions`&&(g=`sessions`),b()},()=>{r.replaceChildren(),v={sessions:[],runs:[],messages:[]},y=``});return window.addEventListener(`pier:theme`,()=>{T.visible&&x()}),Object.assign(T,{refresh(){T.visible&&b()}})}export{h as createActivityView};
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{G as e,H as t,K as n,L as r,N as i,O as a,P as o,R as s,V as c,h as l}from"./index-BrBNzaid.js";var u=e=>e.public&&e.token?`/p/${e.slug}-${e.token}/`:`/boards/${e.slug}/`;function d(d,f){let p=[],m=``,h=r(`header`,`sticky top-0 z-30 flex h-10 items-center gap-3 border-b border-neutral-200 bg-white px-4`,r(`span`,`font-medium max-md:hidden`,`Boards`)),g=r(`div`,`px-4 py-5`);d.append(r(`div`,`min-h-0 flex-1 overflow-y-auto`,h,g));async function _(e,t){let r=await n(`/api/boards/${e.slug}`,{public:t},`PATCH`);if(r.ok)return e.token=(await r.json().catch(()=>({}))).token??``,x();m=await c(r,`Could not change ${e.slug}`),await S()}async function v(t){m=await e(`/api/boards/${t}`,`DELETE`,`Could not delete ${t}`)??``,await S()}function y(e){let t=r(`div`,`group flex flex-col gap-1 border-b border-neutral-200/70 px-1 py-2.5 last:border-b-0`),n=document.createElement(`a`);n.className=`truncate font-medium text-neutral-800 hover:text-indigo-700`,n.href=u(e),n.target=`_blank`,n.rel=`noreferrer`,n.textContent=e.title;let i=r(`div`,`flex items-center gap-2`,n,r(`span`,`flex-none font-mono text-[11.5px] text-neutral-400`,e.slug)),c=a(``,``,e.public,t=>{e.public=t,_(e,t)});c.className=`ml-auto flex flex-none cursor-pointer items-center gap-1.5 text-[11.5px] text-neutral-500`,c.append(r(`span`,``,`Public`));let d=l(`Delete`,`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-red-600 group-hover:opacity-100`);d.title=`Renames the folder on disk; nothing is erased`,d.onclick=()=>void v(e.slug);let p=o(`flex-none cursor-pointer text-[11.5px] text-neutral-400 opacity-0 transition-opacity hover:text-indigo-600 group-hover:opacity-100`,()=>`${location.origin}${u(e)}`);p.title=`Copy the board's link`,i.append(c,p,d),t.append(i);let m=r(`div`,`flex flex-wrap items-center gap-x-3 gap-y-1 text-[11.5px] text-neutral-500`);e.description&&m.append(r(`span`,`min-w-0 truncate`,e.description)),m.append(r(`span`,`flex-none text-neutral-400`,s(Date.parse(e.updatedAt))));for(let t of e.sessions){let e=l(t.slice(0,8),`flex-none cursor-pointer font-mono text-indigo-600 hover:underline`);e.title=`Open session ${t}`,e.onclick=()=>f(t),m.append(e)}if(e.public&&e.token){let t=r(`span`,`flex-none font-mono text-neutral-400`,u(e));t.title=`Public URL — anyone holding this link can read the board, no password`,m.append(t)}return t.append(m),t}let b=[{title:`Public`,hint:`Readable at /p/<slug>-<token>/ by anyone holding the link.`,wanted:!0},{title:`Private`,hint:`Reachable from the Console only.`,wanted:!1}];function x(){let e=r(`div`,`mx-auto flex max-w-3xl flex-col`);p.length===0&&e.append(r(`p`,`rounded-xl border border-dashed border-neutral-300 px-4 py-6 text-center text-[12.5px] text-neutral-500`,`No boards yet. Ask an agent to build one — it writes a folder under ~/.pier/boards.`));for(let{title:t,hint:n,wanted:i}of b){let a=p.filter(e=>e.public===i);if(a.length===0)continue;let o=r(`section`,`mb-6`);o.append(r(`h2`,`text-[13px] font-semibold text-neutral-700`,t)),o.append(r(`p`,`mb-1 text-[11.5px] leading-snug text-neutral-500`,n));let s=r(`div`,`rounded-xl border border-neutral-200 bg-white px-3`);s.append(...a.map(y)),o.append(s),e.append(o)}g.replaceChildren(...m?[r(`p`,`mb-3 text-[13px] text-red-600`,m)]:[],e)}async function S(){let e=await t(`/api/boards`,`Could not load boards`);if(!e.ok)return m=e.error,x();p=e.value,x(),m=``}let C=i(d,()=>void S());return window.addEventListener(`focus`,()=>{C.visible&&S()}),C}export{d as createBoardsView};
|
|
Binary file
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{
|
|
1
|
+
import{L as e,u as t}from"./index-BrBNzaid.js";var n=2e4,r=e=>e.map((e,t)=>({nums:[t+1],text:e,tone:``}));function i(t,r){let i=t.length<=n?r:null,o=Math.max(2,...t.map(e=>e.nums.reduce((e,t)=>Math.max(e,String(t).length),0))),s=e(`div`,`py-2 font-mono text-[12px] leading-[1.5]`);for(let n of t){let t=e(`div`,`flex pl-2 pr-4 ${n.tone===`add`?`bg-emerald-50`:n.tone===`del`?`bg-red-50`:``}`);for(let r of n.nums){let n=e(`span`,`flex-none select-none pr-1.5 text-right text-neutral-300`,r===``?``:String(r));n.style.width=`calc(${o}ch + 0.375rem)`,t.append(n)}t.append(a(n,i)),s.append(t)}return s}function a(n,r){let i=`min-w-0 flex-1 whitespace-pre-wrap [overflow-wrap:anywhere]`;if(!n.mark||n.mark[0]>=n.mark[1]){let e=t(n.text,r);return e.className=i,e}let[a,o]=n.mark,s=t(n.text.slice(a,o),r);return s.className=`rounded-xs ${n.tone===`add`?`bg-emerald-200/80`:`bg-red-200/70`}`,e(`span`,i,t(n.text.slice(0,a),r),s,t(n.text.slice(o),r))}export{r as n,i as t};
|