@timqi/pier 0.1.1 → 0.1.3
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.js +57 -59
- package/dist/agent/listing.js +38 -15
- package/dist/agent/packages.js +353 -0
- package/dist/agent/pi.js +28 -13
- package/dist/core/types.js +7 -0
- package/dist/extensions/index.js +1 -20
- package/dist/main.js +19 -14
- package/dist/settings.js +5 -0
- package/dist/tools.js +46 -85
- package/dist/web/config.js +25 -11
- package/dist/web/explorer.js +5 -3
- package/dist/web/instance.js +6 -18
- package/dist/web/packages.js +84 -0
- package/dist/web/public/assets/{activity-B89_hH7q.js → activity-BSMeRcN2.js} +2 -2
- package/dist/web/public/assets/activity-BSMeRcN2.js.br +0 -0
- package/dist/web/public/assets/activity-BSMeRcN2.js.gz +0 -0
- package/dist/web/public/assets/boards-BCWQMZry.js +1 -0
- package/dist/web/public/assets/boards-BCWQMZry.js.br +0 -0
- package/dist/web/public/assets/boards-BCWQMZry.js.gz +0 -0
- package/dist/web/public/assets/explorer-Cr4XTi4j.js +4 -0
- package/dist/web/public/assets/explorer-Cr4XTi4j.js.br +0 -0
- package/dist/web/public/assets/explorer-Cr4XTi4j.js.gz +0 -0
- package/dist/web/public/assets/index-C7tA0Ufu.js +85 -0
- package/dist/web/public/assets/index-C7tA0Ufu.js.br +0 -0
- package/dist/web/public/assets/index-C7tA0Ufu.js.gz +0 -0
- package/dist/web/public/assets/index-DVIt5Gio.css +2 -0
- package/dist/web/public/assets/index-DVIt5Gio.css.br +0 -0
- package/dist/web/public/assets/index-DVIt5Gio.css.gz +0 -0
- package/dist/web/public/assets/runs-DdERzeac.js +1 -0
- package/dist/web/public/assets/runs-DdERzeac.js.br +0 -0
- package/dist/web/public/assets/runs-DdERzeac.js.gz +0 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js +5 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js.br +0 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js.gz +0 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js +3 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js.br +0 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js.gz +0 -0
- package/dist/web/public/assets/tasks-CY30H1u1.js +4 -0
- package/dist/web/public/assets/tasks-CY30H1u1.js.br +0 -0
- package/dist/web/public/assets/tasks-CY30H1u1.js.gz +0 -0
- package/dist/web/public/index.html +2 -2
- package/dist/web/public/index.html.br +0 -0
- package/dist/web/public/index.html.gz +0 -0
- package/dist/web/server.js +3 -1
- package/docs/deploy.md +11 -0
- package/package.json +1 -1
- package/dist/web/public/assets/activity-B89_hH7q.js.br +0 -0
- package/dist/web/public/assets/activity-B89_hH7q.js.gz +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js +0 -1
- package/dist/web/public/assets/boards-BeKW0ZXK.js.br +0 -0
- package/dist/web/public/assets/boards-BeKW0ZXK.js.gz +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js +0 -4
- package/dist/web/public/assets/explorer-DIuMlaV3.js.br +0 -0
- package/dist/web/public/assets/explorer-DIuMlaV3.js.gz +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js +0 -85
- package/dist/web/public/assets/index-DzXDXra_.js.br +0 -0
- package/dist/web/public/assets/index-DzXDXra_.js.gz +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css +0 -2
- package/dist/web/public/assets/index-eqQLVS8Q.css.br +0 -0
- package/dist/web/public/assets/index-eqQLVS8Q.css.gz +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js +0 -1
- package/dist/web/public/assets/runs-Cwy0mN8i.js.br +0 -0
- package/dist/web/public/assets/runs-Cwy0mN8i.js.gz +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js +0 -5
- package/dist/web/public/assets/settings-DzZLmujq.js.br +0 -0
- package/dist/web/public/assets/settings-DzZLmujq.js.gz +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js +0 -3
- package/dist/web/public/assets/task-runs-BCakxFk8.js.br +0 -0
- package/dist/web/public/assets/task-runs-BCakxFk8.js.gz +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js +0 -4
- package/dist/web/public/assets/tasks-BlzEbk11.js.br +0 -0
- package/dist/web/public/assets/tasks-BlzEbk11.js.gz +0 -0
package/dist/agent/pi.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Implements the AgentFactory/AgentSession seam from src/core/types.ts on the
|
|
3
3
|
// Pi SDK. No Pi type may appear in an exported signature.
|
|
4
4
|
import { realpathSync } from "node:fs";
|
|
5
|
-
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { basename, dirname, join, sep } from "node:path";
|
|
6
6
|
import { createAgentSession, CredentialSynchronizationError, DefaultResourceLoader, defineTool, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { inlineExtensions } from "../extensions/index.js";
|
|
8
8
|
import { SESSION_TITLE_MAX } from "../core/types.js";
|
|
@@ -112,20 +112,30 @@ const bashTimeoutDefault = (pi) => {
|
|
|
112
112
|
}
|
|
113
113
|
});
|
|
114
114
|
};
|
|
115
|
+
/** What the last session open stood down, by bundled extension name: the
|
|
116
|
+
* registry (agent/packages.ts) shows it as the row's state. Known only at
|
|
117
|
+
* open, so a built-in no session has loaded since reads nothing. */
|
|
118
|
+
const shadowed = new Map();
|
|
119
|
+
export const shadowedBuiltin = (name) => shadowed.get(name) ?? null;
|
|
115
120
|
/** Pi loads a bundled extension and its on-disk twin both, leaving two tools
|
|
116
121
|
* of one name; the copy the user put there wins, and the journal says so (§5). */
|
|
117
122
|
export const standDownShadowed = (base) => {
|
|
118
123
|
const inline = (ext) => ext.path.startsWith("<inline:");
|
|
119
|
-
const onDisk = new
|
|
120
|
-
if (!onDisk.size)
|
|
121
|
-
return base;
|
|
124
|
+
const onDisk = new Map(base.extensions.filter((ext) => !inline(ext)).flatMap((ext) => [...ext.tools.keys()].map((tool) => [tool, ext.path])));
|
|
122
125
|
return {
|
|
123
126
|
...base,
|
|
124
127
|
extensions: base.extensions.filter((ext) => {
|
|
125
|
-
|
|
126
|
-
if (!clash || !clash.length)
|
|
128
|
+
if (!inline(ext))
|
|
127
129
|
return true;
|
|
128
|
-
|
|
130
|
+
const name = ext.path.slice("<inline:".length, -1);
|
|
131
|
+
const clash = [...ext.tools.keys()].filter((tool) => onDisk.has(tool));
|
|
132
|
+
if (!clash.length) {
|
|
133
|
+
shadowed.delete(name);
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
const from = onDisk.get(clash[0]);
|
|
137
|
+
shadowed.set(name, `stood down — ${clash.join(", ")} from ${from}`);
|
|
138
|
+
log.info(`bundled ${ext.path} stood down — ${clash.join(", ")} already loaded from ${from}`);
|
|
129
139
|
return false;
|
|
130
140
|
}),
|
|
131
141
|
};
|
|
@@ -396,7 +406,7 @@ export class PiAgentFactory {
|
|
|
396
406
|
credentials;
|
|
397
407
|
providerConfig;
|
|
398
408
|
pinned;
|
|
399
|
-
|
|
409
|
+
pier;
|
|
400
410
|
titleModel;
|
|
401
411
|
listings;
|
|
402
412
|
constructor(extraTools = [],
|
|
@@ -408,7 +418,9 @@ export class PiAgentFactory {
|
|
|
408
418
|
skillPaths = [],
|
|
409
419
|
/** Optional only for bare test factories; an OAuth refresh persists here,
|
|
410
420
|
* never to a plaintext auth.json. */
|
|
411
|
-
credentials, providerConfig = new PiConfigStore(), pinned = () => [],
|
|
421
|
+
credentials, providerConfig = new PiConfigStore(), pinned = () => [],
|
|
422
|
+
/** The built-in `pier` package's switches: bundled extensions on, Pier's own skills off. */
|
|
423
|
+
pier = () => ({ extensions: [], skillsOff: [] }), titleModel = () => undefined,
|
|
412
424
|
/** Injected so a test needs no session directory or database. */
|
|
413
425
|
listings = new IndexedListing()) {
|
|
414
426
|
this.extraTools = extraTools;
|
|
@@ -417,7 +429,7 @@ export class PiAgentFactory {
|
|
|
417
429
|
this.credentials = credentials;
|
|
418
430
|
this.providerConfig = providerConfig;
|
|
419
431
|
this.pinned = pinned;
|
|
420
|
-
this.
|
|
432
|
+
this.pier = pier;
|
|
421
433
|
this.titleModel = titleModel;
|
|
422
434
|
this.listings = listings;
|
|
423
435
|
}
|
|
@@ -605,19 +617,22 @@ export class PiAgentFactory {
|
|
|
605
617
|
await (await this.authRuntime()).logout(providerId, { signal: AbortSignal.timeout(15_000) });
|
|
606
618
|
}
|
|
607
619
|
async resourceLoader(cwd) {
|
|
620
|
+
const { extensions, skillsOff } = this.pier();
|
|
608
621
|
const loader = new DefaultResourceLoader({
|
|
609
622
|
cwd,
|
|
610
623
|
agentDir: defaultAgentDir(),
|
|
611
624
|
// The user's SYSTEM.md is appended after Pier's baseline, so it still wins.
|
|
612
625
|
systemPromptOverride: pierSystemPrompt,
|
|
613
626
|
additionalSkillPaths: this.skillPaths,
|
|
627
|
+
// Only Pier's own skills answer to the off-list; a user's skill of the
|
|
628
|
+
// same name is Pi's to switch (settings.json).
|
|
614
629
|
skillsOverride: (base) => ({
|
|
615
630
|
...base,
|
|
616
|
-
skills: standDownUndocumented(this.extraTools, base.skills),
|
|
631
|
+
skills: standDownUndocumented(this.extraTools, base.skills.filter((skill) => !skillsOff.includes(skill.name) || !this.skillPaths.some((dir) => skill.filePath.startsWith(dir + sep)))),
|
|
617
632
|
}),
|
|
618
633
|
extensionFactories: [
|
|
619
634
|
{ name: "pier-bash-timeout", factory: bashTimeoutDefault, hidden: true },
|
|
620
|
-
...inlineExtensions(
|
|
635
|
+
...inlineExtensions(extensions),
|
|
621
636
|
],
|
|
622
637
|
extensionsOverride: standDownShadowed,
|
|
623
638
|
agentsFilesOverride: (current) => {
|
|
@@ -633,7 +648,7 @@ export class PiAgentFactory {
|
|
|
633
648
|
return loader;
|
|
634
649
|
}
|
|
635
650
|
open(cwd, sessionManager, opts = { cwd }) {
|
|
636
|
-
return this.providerConfig.
|
|
651
|
+
return this.providerConfig.withWrite(() => this.openSnapshot(cwd, sessionManager, opts));
|
|
637
652
|
}
|
|
638
653
|
async openSnapshot(cwd, sessionManager, opts) {
|
|
639
654
|
let live;
|
package/dist/core/types.js
CHANGED
|
@@ -13,6 +13,13 @@ export const SESSION_TITLE_MAX = 80;
|
|
|
13
13
|
* drift, and boundary validators use isThinkingLevel instead of their own copy. */
|
|
14
14
|
export const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh", "max"];
|
|
15
15
|
export const isThinkingLevel = (v) => typeof v === "string" && THINKING_LEVELS.includes(v);
|
|
16
|
+
export class PackageError extends Error {
|
|
17
|
+
reason;
|
|
18
|
+
constructor(reason, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.reason = reason;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
16
23
|
// Wire-protocol names, not SDK types — but they are pi-ai's spellings, and a
|
|
17
24
|
// non-Pi backend is bound to them by this seam.
|
|
18
25
|
const PROVIDER_APIS = [
|
package/dist/extensions/index.js
CHANGED
|
@@ -3,27 +3,8 @@
|
|
|
3
3
|
// either clobbers the user's edits or skips them forever.
|
|
4
4
|
import web from "./web/index.js";
|
|
5
5
|
export const BUNDLED = [
|
|
6
|
-
{
|
|
7
|
-
name: "web",
|
|
8
|
-
summary: "The public web through the provider's own hosted web tools — no extra " +
|
|
9
|
-
"key, no second service, no new dependency.",
|
|
10
|
-
tools: [
|
|
11
|
-
{ name: "web_search", needs: "an authenticated Anthropic or OpenAI model" },
|
|
12
|
-
{ name: "web_fetch", needs: "an authenticated Anthropic model — OpenAI hosts no fetch tool" },
|
|
13
|
-
],
|
|
14
|
-
factory: web,
|
|
15
|
-
},
|
|
6
|
+
{ name: "web", factory: web },
|
|
16
7
|
];
|
|
17
|
-
/** The catalog a surface may show: no Pi types, nothing it cannot render.
|
|
18
|
-
* Half of one list; src/tools.ts has the other. */
|
|
19
|
-
export const bundledInfo = (enabled) => BUNDLED.map(({ name, summary, tools }) => ({
|
|
20
|
-
source: "bundled",
|
|
21
|
-
kind: "extension",
|
|
22
|
-
name,
|
|
23
|
-
summary,
|
|
24
|
-
adds: tools,
|
|
25
|
-
enabled: enabled.includes(name),
|
|
26
|
-
}));
|
|
27
8
|
/** The enabled ones as Pi inline extensions; unknown names are not ours. */
|
|
28
9
|
export const inlineExtensions = (enabled) => BUNDLED.filter((ext) => enabled.includes(ext.name))
|
|
29
10
|
.map(({ name, factory }) => ({ name, factory }));
|
package/dist/main.js
CHANGED
|
@@ -9,6 +9,7 @@ import { ConfigSync } from "./config-sync.js";
|
|
|
9
9
|
import { configSyncTask } from "./config-sync-task.js";
|
|
10
10
|
import { CredentialStore } from "./agent/credentials.js";
|
|
11
11
|
import { IndexedListing } from "./agent/listing.js";
|
|
12
|
+
import { PiPackageStore } from "./agent/packages.js";
|
|
12
13
|
import { PiAgentFactory } from "./agent/pi.js";
|
|
13
14
|
import { defaultBoardsDir, registerBoardRoutes } from "./boards/boards.js";
|
|
14
15
|
import { ChannelStore } from "./channels/config.js";
|
|
@@ -24,7 +25,7 @@ import { EventHub } from "./core/hub.js";
|
|
|
24
25
|
import { splitSpeaker } from "./core/identity.js";
|
|
25
26
|
import { pierDb } from "./db.js";
|
|
26
27
|
import { deliverLedger, drainForRestart, RestartLedger } from "./drain.js";
|
|
27
|
-
import {
|
|
28
|
+
import { BUNDLED } from "./extensions/index.js";
|
|
28
29
|
import { surfacePrompt } from "./core/reply.js";
|
|
29
30
|
import { Router } from "./core/router.js";
|
|
30
31
|
import { acquireInstanceLock } from "./lock.js";
|
|
@@ -39,7 +40,7 @@ import { toolsTask } from "./tools-task.js";
|
|
|
39
40
|
import { Secrets } from "./secrets.js";
|
|
40
41
|
import { startUpdate, unitPath, updaterProblem } from "./service.js";
|
|
41
42
|
import { SettingsStore } from "./settings.js";
|
|
42
|
-
import { startAutoUpdate, UpdateCheck } from "./update.js";
|
|
43
|
+
import { currentVersion, startAutoUpdate, UpdateCheck } from "./update.js";
|
|
43
44
|
import { AuthStore, registerAuthRoutes, requireAuth } from "./web/auth.js";
|
|
44
45
|
import { registerConfigShareRoute, registerConfigSyncRoutes } from "./web/config-sync.js";
|
|
45
46
|
import { PushStore, registerPushRoutes } from "./web/push.js";
|
|
@@ -84,11 +85,14 @@ let channelStore;
|
|
|
84
85
|
const slackDirectory = new SlackDirectory((m) => logger("slack").warn(m));
|
|
85
86
|
let readyForConfigReload = false;
|
|
86
87
|
const piConfig = new PiConfigStore();
|
|
88
|
+
// Before anything reads settings.json: a first boot gets Pier's seed file.
|
|
89
|
+
await piConfig.seedSettings();
|
|
87
90
|
const configSync = new ConfigSync({
|
|
88
91
|
db, settings, config: piConfig, normalizeAgent: normalizeAgentSnapshot,
|
|
89
92
|
reload: () => readyForConfigReload ? reloadInstance() : Promise.resolve(),
|
|
90
93
|
});
|
|
91
|
-
const
|
|
94
|
+
const skillsDir = fileURLToPath(new URL("../skills", import.meta.url));
|
|
95
|
+
const agentTools = [
|
|
92
96
|
taskToolSpec((params, callerSessionId) => tasks.tool(params, callerSessionId)),
|
|
93
97
|
slackToolSpec((params, callerSessionId) => handleSlackTool({
|
|
94
98
|
store: channelStore,
|
|
@@ -108,12 +112,13 @@ const factory = new PiAgentFactory([
|
|
|
108
112
|
},
|
|
109
113
|
log: (m) => logger("slack.tool").warn(m),
|
|
110
114
|
}, params, callerSessionId), () => slackToolAvailable(channelStore)),
|
|
111
|
-
]
|
|
115
|
+
];
|
|
116
|
+
const factory = new PiAgentFactory(agentTools,
|
|
112
117
|
// Getters, read per session open: a Console change reaches the next session
|
|
113
118
|
// without a restart.
|
|
114
119
|
() => surfacePrompt({ boardsDir: defaultBoardsDir(), publicUrl: settings.get().publicUrl }),
|
|
115
120
|
// Documents Pier's own tools, so it loads only inside a Pier session.
|
|
116
|
-
[
|
|
121
|
+
[skillsDir], new CredentialStore(db, secrets), piConfig, () => settings.get().modelMenu, () => settings.get(), () => settings.get().titleModel,
|
|
117
122
|
// Transcripts carry the speaker header core wrote for the model.
|
|
118
123
|
new IndexedListing(undefined, undefined, (text) => splitSpeaker(text).text));
|
|
119
124
|
const hub = new EventHub();
|
|
@@ -137,6 +142,9 @@ const toolsUpdate = toolsTask(tasks);
|
|
|
137
142
|
const reconciled = await toolsUpdate.reconcile();
|
|
138
143
|
if ("problem" in reconciled)
|
|
139
144
|
log.error(`tools cannot be managed: ${reconciled.problem}`);
|
|
145
|
+
const packages = new PiPackageStore(piConfig, { version: currentVersion(), settings, tools: agentTools }, [skillsDir]);
|
|
146
|
+
// At boot, not lazily: the answer waits for the next Console open (update.ts).
|
|
147
|
+
packages.watchUpdates();
|
|
140
148
|
channelStore = new ChannelStore(db, secrets);
|
|
141
149
|
const control = createControl({ router, factory, conversations, store: channelStore });
|
|
142
150
|
const channels = new ChannelRuntime(channelStore, router, control);
|
|
@@ -280,23 +288,20 @@ app.route("/", createServer({
|
|
|
280
288
|
hub,
|
|
281
289
|
sessions: sessionState,
|
|
282
290
|
config: piConfig,
|
|
291
|
+
packages,
|
|
283
292
|
providers: factory,
|
|
284
293
|
settings,
|
|
285
|
-
// Assembled here
|
|
286
|
-
// imports the Pi SDK; extensions and binaries are one kind of switch.
|
|
294
|
+
// Assembled here: the catalog spawns ubix, which web/ may not.
|
|
287
295
|
catalog: async () => {
|
|
288
|
-
const {
|
|
289
|
-
return {
|
|
290
|
-
entries: [...bundledInfo(extensions), ...await managedTools.status(tools, customTools)],
|
|
291
|
-
toolsTaskId: toolsUpdate.id(),
|
|
292
|
-
};
|
|
296
|
+
const { tools, customTools } = settings.get();
|
|
297
|
+
return { entries: await managedTools.status(tools, customTools), toolsTaskId: toolsUpdate.id() };
|
|
293
298
|
},
|
|
294
299
|
// A switch is validated against what this Pier *can* switch, never against
|
|
295
300
|
// a catalog whose custom half the request may be rewriting.
|
|
296
|
-
names:
|
|
301
|
+
names: MANAGED.map((tool) => tool.name),
|
|
297
302
|
onToolsChanged: toolsUpdate.changed,
|
|
298
303
|
validateCustomTools: (raw) => {
|
|
299
|
-
const validated = normalizeCustomTools(raw,
|
|
304
|
+
const validated = normalizeCustomTools(raw, BUNDLED.map((ext) => ext.name));
|
|
300
305
|
return validated ? { tools: validated } : { error: CUSTOM_TOOL_RULES };
|
|
301
306
|
},
|
|
302
307
|
secrets,
|
package/dist/settings.js
CHANGED
|
@@ -99,6 +99,7 @@ export class SettingsStore {
|
|
|
99
99
|
...(titleModel ? { titleModel } : {}),
|
|
100
100
|
autoUpdate: this.#value("autoUpdate") === "1",
|
|
101
101
|
extensions: this.#json("extensions", normalizeExtensions, "a list of names") ?? [],
|
|
102
|
+
skillsOff: this.#json("skillsOff", normalizeNames, "a list of names") ?? [],
|
|
102
103
|
tools: this.#json("tools", normalizeTools, "a list of names") ?? [],
|
|
103
104
|
// `"drop"`: a row the bundled catalog has since taken is redundant, not malformed.
|
|
104
105
|
customTools: this.#json("customTools", (raw) => normalizeCustomTools(raw, [], "drop"), "a list of {name, toml}") ?? [],
|
|
@@ -144,6 +145,10 @@ export class SettingsStore {
|
|
|
144
145
|
this.#set("extensions", JSON.stringify(names));
|
|
145
146
|
return this.get();
|
|
146
147
|
}
|
|
148
|
+
setSkillsOff(names) {
|
|
149
|
+
this.#set("skillsOff", JSON.stringify(names));
|
|
150
|
+
return this.get();
|
|
151
|
+
}
|
|
147
152
|
setTools(names) {
|
|
148
153
|
this.#set("tools", JSON.stringify(names));
|
|
149
154
|
return this.get();
|
package/dist/tools.js
CHANGED
|
@@ -13,37 +13,34 @@ const log = logger("tools");
|
|
|
13
13
|
const UBIX_LATEST = "https://api.github.com/repos/timqi/ubix/releases/latest";
|
|
14
14
|
export const MANAGED = [
|
|
15
15
|
{
|
|
16
|
-
kind: "extension",
|
|
17
16
|
name: "rtk",
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
17
|
+
// The hooks register and unregister its Pi extension; `--auto-patch` keeps
|
|
18
|
+
// `rtk init` non-interactive and overwrites a stale copy, which is right
|
|
19
|
+
// because nothing else writes Pier's agent dir.
|
|
20
|
+
toml: `spec = "github:rtk-ai/rtk"\n` +
|
|
21
|
+
`post_install = ["rtk", "init", "-g", "--agent", "pi", "--auto-patch"]\n` +
|
|
22
|
+
`pre_remove = ["rtk", "init", "--uninstall", "--agent", "pi", "--global", "--auto-patch"]`,
|
|
23
|
+
summary: "Compresses long bash output before it reaches the model. Its install hook " +
|
|
24
|
+
"writes its own Pi extension (extensions/rtk.ts, listed under the local " +
|
|
25
|
+
"package) — refreshed on every update.",
|
|
25
26
|
},
|
|
26
27
|
{
|
|
27
|
-
kind: "tool",
|
|
28
28
|
name: "rg",
|
|
29
29
|
// `exe`: ubi looks for files named after the project, and ripgrep ships `rg`.
|
|
30
30
|
toml: `spec = "github:BurntSushi/ripgrep"\nexe = "rg"`,
|
|
31
31
|
summary: "ripgrep: searches a tree by content, fast enough to be the default.",
|
|
32
32
|
},
|
|
33
33
|
{
|
|
34
|
-
kind: "tool",
|
|
35
34
|
name: "fd",
|
|
36
35
|
toml: `spec = "github:sharkdp/fd"`,
|
|
37
36
|
summary: "Finds files by name, respecting .gitignore — what `find` should feel like.",
|
|
38
37
|
},
|
|
39
38
|
{
|
|
40
|
-
kind: "tool",
|
|
41
39
|
name: "wt",
|
|
42
40
|
toml: `spec = "github:max-sixty/worktrunk"\nexe = "wt"`,
|
|
43
41
|
summary: "worktrunk: git worktrees as one command — branch, switch, merge, clean up.",
|
|
44
42
|
},
|
|
45
43
|
{
|
|
46
|
-
kind: "tool",
|
|
47
44
|
name: "jq",
|
|
48
45
|
// No `exe`: jq publishes bare per-platform binaries, which ubi installs
|
|
49
46
|
// under the tool's own name.
|
|
@@ -339,17 +336,14 @@ export function ubixConfigToml(tools, installDir) {
|
|
|
339
336
|
lines.push("", `[tools.${tool.name}]`, tool.toml.trim());
|
|
340
337
|
return `${lines.join("\n")}\n`;
|
|
341
338
|
}
|
|
342
|
-
/**
|
|
343
|
-
*
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
/** Pier put that binary in `bin/`, so `sync` re-bootstraps rather than reports. */
|
|
347
|
-
class UbixTooOld extends Error {
|
|
348
|
-
}
|
|
339
|
+
/** The first ubix that runs `post_install` / `pre_remove`. An older one parses
|
|
340
|
+
* the keys as unknown and ignores them, so rtk would install without its
|
|
341
|
+
* extension and nothing would say so. Release tags are `vYYYYMMDD-<sha>`. */
|
|
342
|
+
const UBIX_MIN = "v20260910-61f07ae";
|
|
349
343
|
/** A name in neither list is not an error: a row a future release drops must
|
|
350
344
|
* not stop the sync of everything else. */
|
|
351
345
|
function rows(custom) {
|
|
352
|
-
return [...MANAGED, ...custom.map((tool) => ({
|
|
346
|
+
return [...MANAGED, ...custom.map((tool) => ({ summary: "", custom: true, ...tool }))];
|
|
353
347
|
}
|
|
354
348
|
export function ubixAsset(tag, platform, arch) {
|
|
355
349
|
const os = platform === "linux" || platform === "darwin" ? platform : null;
|
|
@@ -465,26 +459,11 @@ export class ManagedTools {
|
|
|
465
459
|
return { entries: [], failed: false, summary: "no tools switched on" };
|
|
466
460
|
}
|
|
467
461
|
fence();
|
|
468
|
-
const ubix = await this.bootstrapUbix();
|
|
469
462
|
const env = this.#env();
|
|
463
|
+
const ubix = await this.#current(await this.bootstrapUbix(), env);
|
|
470
464
|
const entries = [];
|
|
471
|
-
// ubix cannot know that rtk must uninstall its own Pi extension *before*
|
|
472
|
-
// its binary goes; tools leaving the set undo their footprint first.
|
|
473
|
-
const kept = [];
|
|
474
|
-
for (const state of await this.#listing(ubix, env)) {
|
|
475
|
-
const leaving = all.find((tool) => tool.name === state.name);
|
|
476
|
-
if (!leaving?.deprovision || wanted.some((tool) => tool.name === state.name))
|
|
477
|
-
continue;
|
|
478
|
-
fence(); // a tool's own uninstall is a change to the machine
|
|
479
|
-
const error = await this.#provision(env, leaving, leaving.deprovision);
|
|
480
|
-
if (error) {
|
|
481
|
-
// Removing it now would orphan what deprovision failed to remove.
|
|
482
|
-
kept.push(leaving);
|
|
483
|
-
entries.push({ name: leaving.name, action: "kept", version: null, error });
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
465
|
fence();
|
|
487
|
-
this.#writeConfig(
|
|
466
|
+
this.#writeConfig(wanted);
|
|
488
467
|
// `--prune` removes what the config no longer declares; `--wait` lets a
|
|
489
468
|
// hand-typed `pier tools sync` converge behind the managed run.
|
|
490
469
|
fence();
|
|
@@ -492,25 +471,19 @@ export class ManagedTools {
|
|
|
492
471
|
for (const state of states) {
|
|
493
472
|
if (wanted.some((tool) => tool.name === state.name))
|
|
494
473
|
continue;
|
|
495
|
-
|
|
496
|
-
continue;
|
|
497
|
-
// A failure to remove is as much a failure as one to install.
|
|
474
|
+
// A failure to remove (a `pre_remove` hook included) is as much a failure as one to install.
|
|
498
475
|
entries.push({ name: state.name, action: state.action ?? "removed", version: null, error: state.error });
|
|
499
476
|
}
|
|
500
477
|
for (const tool of wanted) {
|
|
501
478
|
const state = states.find((s) => s.name === tool.name);
|
|
502
|
-
|
|
479
|
+
entries.push({
|
|
503
480
|
name: tool.name,
|
|
504
481
|
action: state?.action ?? "missing",
|
|
505
482
|
version: state?.to ?? state?.version ?? null,
|
|
506
|
-
// A tool ubix never mentioned is not a tool that is fine
|
|
483
|
+
// A tool ubix never mentioned is not a tool that is fine; a failed
|
|
484
|
+
// `post_install` arrives here as `error` beside an `installed` action.
|
|
507
485
|
error: state?.error ?? (state ? null : "ubix reported nothing about it"),
|
|
508
|
-
};
|
|
509
|
-
if (!entry.error && tool.provision) {
|
|
510
|
-
fence();
|
|
511
|
-
entry.error = await this.#provision(env, tool, tool.provision);
|
|
512
|
-
}
|
|
513
|
-
entries.push(entry);
|
|
486
|
+
});
|
|
514
487
|
}
|
|
515
488
|
const failed = entries.some((entry) => entry.error !== null);
|
|
516
489
|
return { entries, failed, summary: summarize(entries) };
|
|
@@ -519,8 +492,6 @@ export class ManagedTools {
|
|
|
519
492
|
* version is unknown (§5). */
|
|
520
493
|
async status(enabled, custom = []) {
|
|
521
494
|
const base = rows(custom).map((tool) => ({
|
|
522
|
-
source: "binary",
|
|
523
|
-
kind: tool.kind,
|
|
524
495
|
name: tool.name,
|
|
525
496
|
summary: tool.summary,
|
|
526
497
|
enabled: enabled.includes(tool.name),
|
|
@@ -577,12 +548,20 @@ export class ManagedTools {
|
|
|
577
548
|
return states;
|
|
578
549
|
}
|
|
579
550
|
/** `UBIX_CONFIG_DIR` / `UBIX_DATA_DIR` name the directories directly — not
|
|
580
|
-
* XDG parents, which every child ubix spawns (uv, fnm, cargo) would read too.
|
|
551
|
+
* XDG parents, which every child ubix spawns (uv, fnm, cargo) would read too.
|
|
552
|
+
* Hooks inherit this env: `pier tools sync` typed in a shell has no main.ts
|
|
553
|
+
* parent exporting PI_CODING_AGENT_DIR, and rtk would then write its
|
|
554
|
+
* extension into ~/.pi. */
|
|
581
555
|
#env() {
|
|
556
|
+
const agentDir = resolveAgentDir(process.env);
|
|
557
|
+
if (process.env.PI_CODING_AGENT_DIR !== agentDir) {
|
|
558
|
+
log.info(`PI_CODING_AGENT_DIR was ${process.env.PI_CODING_AGENT_DIR ?? "unset"} — ubix gets ${agentDir}`);
|
|
559
|
+
}
|
|
582
560
|
const env = {
|
|
583
561
|
...process.env,
|
|
584
562
|
UBIX_CONFIG_DIR: this.#configDir,
|
|
585
563
|
UBIX_DATA_DIR: join(this.#root, "state"),
|
|
564
|
+
PI_CODING_AGENT_DIR: agentDir,
|
|
586
565
|
};
|
|
587
566
|
prependPath(env, this.bin);
|
|
588
567
|
return env;
|
|
@@ -599,8 +578,9 @@ export class ManagedTools {
|
|
|
599
578
|
renameSync(`${path}.writing`, path);
|
|
600
579
|
}
|
|
601
580
|
/** A non-zero exit still parses: under `--json` a failed tool is in the
|
|
602
|
-
* document as `action: "failed"
|
|
603
|
-
*
|
|
581
|
+
* document as `action: "failed"` — or as `installed`/`upgraded` with an
|
|
582
|
+
* `error`, when its `post_install` hook failed — and the run exits non-zero.
|
|
583
|
+
* The two must agree — an exit code with no failed entry is a failure this file cannot
|
|
604
584
|
* attribute, and passing it on as clean is the one thing it may never do. */
|
|
605
585
|
async #states(ubix, env, args) {
|
|
606
586
|
const result = await this.#exec(ubix, args, env);
|
|
@@ -610,43 +590,24 @@ export class ManagedTools {
|
|
|
610
590
|
}
|
|
611
591
|
catch (err) {
|
|
612
592
|
const failure = failedRun(`ubix ${args.join(" ")}`, result);
|
|
613
|
-
// Only the flag being unknown means "too old"; re-bootstrapping over a
|
|
614
|
-
// config error would fix nothing and say something false.
|
|
615
|
-
if (result.code !== 0 && refusesJson(result.stderr))
|
|
616
|
-
throw new UbixTooOld(failure);
|
|
617
593
|
throw new Error(result.code === 0 ? String(err) : `${failure} (${String(err)})`);
|
|
618
594
|
}
|
|
619
|
-
if (result.code !== 0 && !states.some((state) => state.
|
|
595
|
+
if (result.code !== 0 && !states.some((state) => state.error !== null)) {
|
|
620
596
|
throw new Error(`${failedRun(`ubix ${args.join(" ")}`, result)} — and its report names no failure`);
|
|
621
597
|
}
|
|
622
598
|
return states;
|
|
623
599
|
}
|
|
624
|
-
/** The one place a too-old ubix is repaired rather than reported
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
}
|
|
636
|
-
/** Returns the failure text, or null. */
|
|
637
|
-
async #provision(env, tool, args) {
|
|
638
|
-
const exe = join(this.bin, tool.name);
|
|
639
|
-
if (!existsSync(exe))
|
|
640
|
-
return `${tool.name} is not in ${this.bin} — ${args.join(" ")} was not run`;
|
|
641
|
-
// `pier tools sync` typed in a shell has no main.ts parent exporting
|
|
642
|
-
// PI_CODING_AGENT_DIR; rtk would then write its extension into ~/.pi.
|
|
643
|
-
const agentDir = resolveAgentDir(env);
|
|
644
|
-
if (env.PI_CODING_AGENT_DIR !== agentDir) {
|
|
645
|
-
log.info(`PI_CODING_AGENT_DIR was ${env.PI_CODING_AGENT_DIR ?? "unset"} — ${tool.name} gets ${agentDir}`);
|
|
646
|
-
}
|
|
647
|
-
mkdirSync(join(agentDir, "extensions"), { recursive: true });
|
|
648
|
-
const result = await this.#exec(exe, args, { ...env, PI_CODING_AGENT_DIR: agentDir });
|
|
649
|
-
return result.code === 0 ? null : failedRun(`${tool.name} ${args.join(" ")}`, result);
|
|
600
|
+
/** The one place a too-old ubix is repaired rather than reported: Pier put
|
|
601
|
+
* that binary in `bin/`, and one below UBIX_MIN would run the config and
|
|
602
|
+
* skip its hooks without a word. */
|
|
603
|
+
async #current(ubix, env) {
|
|
604
|
+
const result = await this.#exec(ubix, ["--version"], env);
|
|
605
|
+
const date = /\bv(\d{8})-/.exec(result.stdout)?.[1];
|
|
606
|
+
if (result.code === 0 && date && date >= UBIX_MIN.slice(1, 9))
|
|
607
|
+
return ubix;
|
|
608
|
+
const found = result.code === 0 ? result.stdout.trim() || "(no version)" : failedRun("ubix --version", result);
|
|
609
|
+
log.warn(`the ubix in ${this.bin} is ${found} and Pier needs ${UBIX_MIN} or newer — replacing it`);
|
|
610
|
+
return this.bootstrapUbix(true);
|
|
650
611
|
}
|
|
651
612
|
async #getText(url) {
|
|
652
613
|
return new TextDecoder().decode(await this.#getBytes(url));
|
|
@@ -662,7 +623,7 @@ export class ManagedTools {
|
|
|
662
623
|
return new Uint8Array(await res.arrayBuffer());
|
|
663
624
|
}
|
|
664
625
|
}
|
|
665
|
-
const withBinary = (entry, patch) =>
|
|
626
|
+
const withBinary = (entry, patch) => ({ ...entry, binary: { ...entry.binary, ...patch } });
|
|
666
627
|
/** Never empty: an exit code with no words is not a report. */
|
|
667
628
|
const failedRun = (what, result) => `${what} exited ${String(result.code)}: ${result.stderr.trim().slice(0, 300) || "(no output)"}`;
|
|
668
629
|
/** `<sha256> <bare filename>` lines, as `sha256sum` writes them. */
|
package/dist/web/config.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// The agent files a scope is configured by, through the ConfigStore. Not via
|
|
2
2
|
// web/fs.ts: a scope is "global" or a cwd Pi already knows, never a browser path.
|
|
3
|
+
import { isThinkingLevel } from "../core/types.js";
|
|
4
|
+
import { normalizeModelRef } from "../settings.js";
|
|
3
5
|
import { guarded } from "./route.js";
|
|
4
6
|
export function registerConfigRoutes(app, { factory, config, onConfigWritten }) {
|
|
5
7
|
// Only cwds Pi already knows are accepted — never an arbitrary path.
|
|
@@ -18,7 +20,6 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
18
20
|
// Where this scope's files live on disk — the UI labels "Global" with it.
|
|
19
21
|
dir: scope.kind === "global" ? config.globalDir : scope.cwd,
|
|
20
22
|
files: await config.listFiles(scope),
|
|
21
|
-
resources: await config.listResources(scope),
|
|
22
23
|
});
|
|
23
24
|
});
|
|
24
25
|
guarded(app, "GET", "/api/config/files/:name", 400, async (c) => {
|
|
@@ -41,17 +42,30 @@ export function registerConfigRoutes(app, { factory, config, onConfigWritten })
|
|
|
41
42
|
onConfigWritten?.();
|
|
42
43
|
return c.json({ ok: true, content: await config.readFile(scope, name) });
|
|
43
44
|
});
|
|
44
|
-
//
|
|
45
|
-
guarded(app, "GET", "/api/config/
|
|
45
|
+
// settings.json is read-only as a file; its two deployment keys go through here.
|
|
46
|
+
guarded(app, "GET", "/api/config/defaults", 400, async (c) => {
|
|
46
47
|
c.header("cache-control", "no-store");
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
return c.json({ error: "kind and name required" }, 400);
|
|
48
|
+
return c.json(await config.readDefaults());
|
|
49
|
+
});
|
|
50
|
+
guarded(app, "PUT", "/api/config/defaults", 400, async (c) => {
|
|
51
|
+
const defaults = parseDefaults(await c.req.json().catch(() => null));
|
|
52
|
+
if (!defaults) {
|
|
53
|
+
return c.json({ error: "defaultModel must be {provider, id} or null; defaultThinkingLevel a reasoning level or null" }, 400);
|
|
54
54
|
}
|
|
55
|
-
|
|
55
|
+
await config.writeDefaults(defaults);
|
|
56
|
+
onConfigWritten?.();
|
|
57
|
+
return c.json(await config.readDefaults());
|
|
56
58
|
});
|
|
57
59
|
}
|
|
60
|
+
/** Both fields, each stated: null is "Pi's own default", absent is a malformed body. */
|
|
61
|
+
function parseDefaults(body) {
|
|
62
|
+
if (typeof body !== "object" || body === null)
|
|
63
|
+
return null;
|
|
64
|
+
const { defaultModel, defaultThinkingLevel } = body;
|
|
65
|
+
const model = defaultModel === null ? null : normalizeModelRef(defaultModel);
|
|
66
|
+
if (model === null && defaultModel !== null)
|
|
67
|
+
return null;
|
|
68
|
+
if (defaultThinkingLevel !== null && !isThinkingLevel(defaultThinkingLevel))
|
|
69
|
+
return null;
|
|
70
|
+
return { defaultModel: model, defaultThinkingLevel };
|
|
71
|
+
}
|
package/dist/web/explorer.js
CHANGED
|
@@ -11,7 +11,7 @@ const REF_RE = /^[^-\s][^\s]*$/;
|
|
|
11
11
|
/** Output capped: a diff is display data, not an archive. */
|
|
12
12
|
const git = async (root, ...args) => (await run("git", ["-C", root, ...args], { maxBuffer: MAX_DIFF_BYTES })).stdout;
|
|
13
13
|
export function registerExplorerRoutes(app) {
|
|
14
|
-
// Not a repo → { branch: null }
|
|
14
|
+
// Not a repo → { branch: null }: the Files view draws no compare, no filter.
|
|
15
15
|
guarded(app, "GET", "/api/explorer/git", 404, async (c) => {
|
|
16
16
|
c.header("cache-control", "no-store");
|
|
17
17
|
const root = await scoped(c.req.query("root"));
|
|
@@ -71,9 +71,11 @@ export function registerExplorerRoutes(app) {
|
|
|
71
71
|
const file = c.req.query("file");
|
|
72
72
|
if (file === undefined) {
|
|
73
73
|
// name-status carries the letter, numstat the +/- counts; joined by path.
|
|
74
|
+
// --relative: the root may be a directory inside the checkout (Browse
|
|
75
|
+
// files on a skill), and the tree reads every path from that root.
|
|
74
76
|
const [nameStatus, numstat] = await Promise.all([
|
|
75
|
-
git(root, "diff", "--name-status", ...range, "--"),
|
|
76
|
-
git(root, "diff", "--numstat", ...range, "--"),
|
|
77
|
+
git(root, "diff", "--relative", "--name-status", ...range, "--"),
|
|
78
|
+
git(root, "diff", "--relative", "--numstat", ...range, "--"),
|
|
77
79
|
]);
|
|
78
80
|
// numstat spells a rename "a/{old => new}.ts" — reduce it to the new path.
|
|
79
81
|
const newPath = (p) => p.replace(/\{([^{}]*) => ([^{}]*)\}/g, "$2").replace(/^(.*) => (.*)$/, "$2");
|