@timqi/pier 0.1.3 → 0.1.5
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 +12 -7
- package/dist/agent/config.js +9 -3
- package/dist/agent/events.js +13 -5
- package/dist/agent/listing.js +10 -2
- package/dist/agent/packages.js +4 -20
- package/dist/agent/pi.js +18 -86
- package/dist/channels/attach.js +2 -2
- package/dist/channels/commands.js +10 -8
- package/dist/channels/config.js +32 -24
- package/dist/channels/control.js +60 -14
- package/dist/channels/conversations.js +60 -13
- package/dist/channels/handoff.js +90 -0
- package/dist/channels/lark-api.js +7 -0
- package/dist/channels/lark-outbound.js +19 -0
- package/dist/channels/lark-panel.js +41 -26
- package/dist/channels/lark-render.js +2 -1
- package/dist/channels/lark.js +35 -26
- package/dist/channels/lines.js +1 -1
- package/dist/channels/panel.js +324 -115
- package/dist/channels/receipts.js +25 -12
- package/dist/channels/routes.js +21 -7
- package/dist/channels/runtime.js +21 -7
- package/dist/channels/slack-api.js +19 -10
- package/dist/channels/slack-cli.js +503 -0
- package/dist/channels/slack-directory.js +2 -2
- package/dist/channels/slack-outbound.js +11 -1
- package/dist/channels/slack-panel.js +81 -40
- package/dist/channels/slack-render.js +3 -1
- package/dist/channels/slack-thread.js +41 -0
- package/dist/channels/slack-transcript.js +107 -0
- package/dist/channels/slack.js +38 -31
- package/dist/channels/types.js +1 -3
- package/dist/cli.js +166 -7
- package/dist/core/identity.js +61 -17
- package/dist/core/reply.js +19 -15
- package/dist/core/router.js +23 -3
- package/dist/db.js +33 -0
- package/dist/main.js +61 -41
- package/dist/paths.js +3 -0
- package/dist/secrets.js +2 -1
- package/dist/settings.js +3 -12
- package/dist/socket.js +99 -0
- package/dist/tasks/agent.js +5 -6
- package/dist/tasks/callbacks.js +2 -2
- package/dist/tasks/cli.js +225 -0
- package/dist/tasks/definitions.js +3 -3
- package/dist/tasks/execution.js +1 -3
- package/dist/tasks/groups.js +2 -5
- package/dist/tasks/messages.js +18 -158
- package/dist/tasks/operations.js +335 -0
- package/dist/tasks/routes.js +1 -11
- package/dist/tasks/runs.js +5 -20
- package/dist/tasks/service.js +18 -43
- package/dist/tasks/store.js +15 -15
- package/dist/tools.js +18 -3
- package/dist/vault.js +107 -0
- package/dist/web/auth.js +2 -1
- package/dist/web/public/assets/{activity-BSMeRcN2.js → activity-CrybM-E8.js} +2 -2
- package/dist/web/public/assets/activity-CrybM-E8.js.br +0 -0
- package/dist/web/public/assets/activity-CrybM-E8.js.gz +0 -0
- package/dist/web/public/assets/{boards-BCWQMZry.js → boards-Cw7_6J6L.js} +1 -1
- package/dist/web/public/assets/boards-Cw7_6J6L.js.br +0 -0
- package/dist/web/public/assets/boards-Cw7_6J6L.js.gz +0 -0
- package/dist/web/public/assets/{explorer-Cr4XTi4j.js → explorer-DV066dUD.js} +1 -1
- package/dist/web/public/assets/explorer-DV066dUD.js.br +0 -0
- package/dist/web/public/assets/explorer-DV066dUD.js.gz +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js +85 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.br +0 -0
- package/dist/web/public/assets/index-BrNHu2qj.js.gz +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css +2 -0
- package/dist/web/public/assets/index-DLszkDUV.css.br +0 -0
- package/dist/web/public/assets/index-DLszkDUV.css.gz +0 -0
- package/dist/web/public/assets/{runs-DdERzeac.js → runs-DDagTNaM.js} +1 -1
- package/dist/web/public/assets/runs-DDagTNaM.js.br +0 -0
- package/dist/web/public/assets/runs-DDagTNaM.js.gz +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js +5 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.br +0 -0
- package/dist/web/public/assets/settings-BEdSeXpm.js.gz +0 -0
- package/dist/web/public/assets/{task-runs-C-dGUDsH.js → task-runs-BnMack9t.js} +1 -1
- package/dist/web/public/assets/task-runs-BnMack9t.js.br +0 -0
- package/dist/web/public/assets/task-runs-BnMack9t.js.gz +0 -0
- package/dist/web/public/assets/{tasks-CY30H1u1.js → tasks-BkW7YShZ.js} +1 -1
- package/dist/web/public/assets/tasks-BkW7YShZ.js.br +0 -0
- package/dist/web/public/assets/tasks-BkW7YShZ.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/push.js +4 -10
- package/dist/web/vault.js +68 -0
- package/dist/{extensions/web → websearch}/artifacts.js +2 -2
- package/dist/websearch/cli.js +73 -0
- package/dist/websearch/run.js +273 -0
- package/docs/deploy.md +34 -11
- package/package.json +2 -3
- package/skills/pier-help/SKILL.md +32 -16
- package/skills/pier-slack/SKILL.md +52 -71
- package/skills/pier-tasks/SKILL.md +60 -148
- package/skills/pier-vault/SKILL.md +36 -0
- package/skills/pier-web/SKILL.md +50 -0
- package/dist/channels/slack-tool.js +0 -416
- package/dist/channels/telegram-api.js +0 -86
- package/dist/channels/telegram-panel.js +0 -97
- package/dist/channels/telegram-render.js +0 -69
- package/dist/channels/telegram.js +0 -421
- package/dist/extensions/index.js +0 -10
- package/dist/extensions/web/index.js +0 -9
- package/dist/extensions/web/tools.js +0 -265
- package/dist/tasks/tool.js +0 -416
- 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.br +0 -0
- package/dist/web/public/assets/boards-BCWQMZry.js.gz +0 -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 +0 -85
- 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 +0 -2
- 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.br +0 -0
- package/dist/web/public/assets/runs-DdERzeac.js.gz +0 -0
- package/dist/web/public/assets/settings-CQDAHoMM.js +0 -5
- 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.br +0 -0
- package/dist/web/public/assets/task-runs-C-dGUDsH.js.gz +0 -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/{extensions/web → websearch}/anthropic.js +0 -0
- /package/dist/{extensions/web → websearch}/content.js +0 -0
- /package/dist/{extensions/web → websearch}/http.js +0 -0
- /package/dist/{extensions/web → websearch}/json.js +0 -0
- /package/dist/{extensions/web → websearch}/language.js +0 -0
- /package/dist/{extensions/web → websearch}/openai.js +0 -0
- /package/dist/{extensions/web → websearch}/provider.js +0 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
A self-hosted workspace for coding agents. Pier puts a web workbench and your
|
|
4
4
|
IM channels in front of [Pi](https://github.com/earendil-works/pi) sessions:
|
|
5
|
-
talk to the same agent from a browser, Slack
|
|
5
|
+
talk to the same agent from a browser, Slack or Lark, steer a running
|
|
6
6
|
turn, schedule tasks, watch every session, and publish a static page when
|
|
7
7
|
something is worth showing.
|
|
8
8
|
|
|
@@ -37,8 +37,8 @@ otherwise).
|
|
|
37
37
|
**The first start generates a password and prints it once.** Lost it?
|
|
38
38
|
`sqlite3 ~/.pier/db/pier.db 'DELETE FROM auth'` and restart. Open
|
|
39
39
|
`http://localhost:3141`, sign in; **Console → Settings** configures Models,
|
|
40
|
-
Agent, Channels, the public URL, password and master key; **New session**
|
|
41
|
-
the directory the agent's shell runs in.
|
|
40
|
+
Agent, Channels, Vault, the public URL, password and master key; **New session**
|
|
41
|
+
picks the directory the agent's shell runs in.
|
|
42
42
|
|
|
43
43
|
## Configure Pi
|
|
44
44
|
|
|
@@ -62,12 +62,17 @@ Console → Settings:
|
|
|
62
62
|
`models.json` structure, globally or per project scope (with that project's
|
|
63
63
|
`.pi/skills` and `.pi/extensions` listed). Changes apply when a session next
|
|
64
64
|
opens; saving recycles idle sessions; **Settings → Instance → Reload** does
|
|
65
|
-
the same for files changed elsewhere. Also here:
|
|
66
|
-
(`web
|
|
67
|
-
|
|
65
|
+
the same for files changed elsewhere. Also here: Pier's own skills
|
|
66
|
+
(`pier web search|fetch` reaches the web through the provider's hosted
|
|
67
|
+
tools — Anthropic or OpenAI, no key of your own) and the
|
|
68
68
|
managed CLI tools (`rtk`, `rg`, `fd`, `wt`, `jq`, or your own as a
|
|
69
69
|
[ubix](https://github.com/timqi/ubix) block), installed into
|
|
70
|
-
`~/.pier/tools/bin`, first on every session's PATH
|
|
70
|
+
`~/.pier/tools/bin`, first on every session's PATH beside the `pier` shim
|
|
71
|
+
Pier writes there at start.
|
|
72
|
+
- **Vault** — named secrets an agent's commands receive through `pier vault
|
|
73
|
+
run` without the value entering its context (`docs/design/07-vault.md`);
|
|
74
|
+
`pier slack` and `pier task` reach the running Pier the same way
|
|
75
|
+
(`docs/design/08-cli-socket.md`).
|
|
71
76
|
|
|
72
77
|
On first credential access an existing `auth.json` is imported into the sealed
|
|
73
78
|
store and renamed `auth.json.imported`; literal keys in `models.json` likewise
|
package/dist/agent/config.js
CHANGED
|
@@ -15,9 +15,15 @@ const PROJECT_FILES = ["AGENTS.md"];
|
|
|
15
15
|
const READONLY_FILES = ["settings.json"];
|
|
16
16
|
const SNAPSHOT_FILES = ["SYSTEM.md", "AGENTS.md", "models.json", "settings.json"];
|
|
17
17
|
/** What a first boot writes (docs/deploy.md says why each key): the list the
|
|
18
|
-
* Console installs into,
|
|
19
|
-
* person to survey
|
|
20
|
-
|
|
18
|
+
* Console installs into, Pi's attribution headers off — a server is not a
|
|
19
|
+
* person to survey — and a retry budget of minutes, because an unattended
|
|
20
|
+
* instance has nobody to re-ask after Pi's 14-second default gives up. Every
|
|
21
|
+
* other key stays Pi's default by omission. */
|
|
22
|
+
const SEED_SETTINGS = {
|
|
23
|
+
packages: [],
|
|
24
|
+
enableInstallTelemetry: false,
|
|
25
|
+
retry: { maxRetries: 5, baseDelayMs: 5000 },
|
|
26
|
+
};
|
|
21
27
|
/** Pier owns the Pi runtime dir; main.ts exports it as PI_CODING_AGENT_DIR. */
|
|
22
28
|
export const defaultAgentDir = () => process.env.PI_CODING_AGENT_DIR ?? pierPath("pi");
|
|
23
29
|
/** Stable mask: mapped back by field, without exposing key fragments. */
|
package/dist/agent/events.js
CHANGED
|
@@ -33,8 +33,7 @@ function systemOrigin(message) {
|
|
|
33
33
|
}
|
|
34
34
|
if (origin.kind === "task-message" &&
|
|
35
35
|
typeof origin.messageId === "string" &&
|
|
36
|
-
(origin.messageKind === "steer" || origin.messageKind === "follow_up"
|
|
37
|
-
origin.messageKind === "progress" || origin.messageKind === "decision" || origin.messageKind === "reply"))
|
|
36
|
+
(origin.messageKind === "steer" || origin.messageKind === "follow_up"))
|
|
38
37
|
return shape;
|
|
39
38
|
return null;
|
|
40
39
|
}
|
|
@@ -201,9 +200,14 @@ export function toSessionEvents(e) {
|
|
|
201
200
|
return [];
|
|
202
201
|
// Carried twice: on turn-end because it is how the turn ended (what a
|
|
203
202
|
// task run settles on), and as the error event every chat surface reports.
|
|
203
|
+
// `toolUse` without a tool call is a provider protocol violation (the
|
|
204
|
+
// block was billed but never delivered); Pi ends the run on it, so it
|
|
205
|
+
// reaches the conversation as the failure it is, not as an empty turn.
|
|
204
206
|
const failure = final?.stopReason === "error"
|
|
205
207
|
? final.errorMessage || "unknown agent error"
|
|
206
|
-
:
|
|
208
|
+
: final?.stopReason === "toolUse" && !hasToolCalls(final)
|
|
209
|
+
? "the provider stopped on tool_use but sent no tool call"
|
|
210
|
+
: undefined;
|
|
207
211
|
const out = [
|
|
208
212
|
{ type: "turn-end", text: hasToolCalls(final) ? "" : textOf(final?.content), ...(failure ? { error: failure } : {}) },
|
|
209
213
|
];
|
|
@@ -229,8 +233,12 @@ export function toSessionEvents(e) {
|
|
|
229
233
|
// On the message, not its text: an attachment with no caption is still a turn.
|
|
230
234
|
const out = [{ type: "turn-start" }];
|
|
231
235
|
const text = textOf(m.content);
|
|
232
|
-
if (text)
|
|
233
|
-
out.push(origin
|
|
236
|
+
if (text) {
|
|
237
|
+
out.push(origin
|
|
238
|
+
// `at` is what `turnMetaAt` will read back as this turn's start.
|
|
239
|
+
? { type: "system-input", text, origin, at: m.timestamp }
|
|
240
|
+
: { type: "user-message", text });
|
|
241
|
+
}
|
|
234
242
|
return out;
|
|
235
243
|
}
|
|
236
244
|
case "message_update": {
|
package/dist/agent/listing.js
CHANGED
|
@@ -15,6 +15,13 @@ const log = logger("agent");
|
|
|
15
15
|
const AUDIT_SAMPLE = 5;
|
|
16
16
|
const titleOf = (name, first) => name || first || undefined;
|
|
17
17
|
const str = (v) => (typeof v === "string" ? v : undefined);
|
|
18
|
+
/** The budget is the words, not the header: a 34-character Lark id would spend
|
|
19
|
+
* it all and leave a clip that no longer parses back as a header. */
|
|
20
|
+
function clipTitle(whole, clean) {
|
|
21
|
+
const body = clean(whole);
|
|
22
|
+
const header = whole.slice(0, whole.length - body.length);
|
|
23
|
+
return header + body.slice(0, SESSION_TITLE_MAX);
|
|
24
|
+
}
|
|
18
25
|
/** A reply that calls a tool is a step, not a reply (events.ts), and steps are
|
|
19
26
|
* not indexed. `clean` takes off the speaker header (core/identity.ts), which
|
|
20
27
|
* as text would make "operator" hit every session. */
|
|
@@ -67,7 +74,7 @@ function fold(acc, line, clean) {
|
|
|
67
74
|
// through core/identity.ts.
|
|
68
75
|
if (acc.first || said.role !== "user")
|
|
69
76
|
return acc;
|
|
70
|
-
return { ...acc, first: textOf(message?.content).trim()
|
|
77
|
+
return { ...acc, first: clipTitle(textOf(message?.content).trim(), clean) };
|
|
71
78
|
}
|
|
72
79
|
return acc;
|
|
73
80
|
}
|
|
@@ -201,7 +208,8 @@ export class IndexedListing {
|
|
|
201
208
|
const want = info && {
|
|
202
209
|
cwd: info.cwd,
|
|
203
210
|
created: info.created.getTime(),
|
|
204
|
-
title: info.name ??
|
|
211
|
+
title: info.name ??
|
|
212
|
+
(info.firstMessage ? clipTitle(info.firstMessage.trim(), this.clean) : undefined),
|
|
205
213
|
};
|
|
206
214
|
if (want && want.cwd === seen.cwd && want.created === seen.created &&
|
|
207
215
|
want.title === seen.title)
|
package/dist/agent/packages.js
CHANGED
|
@@ -6,16 +6,12 @@ import { existsSync, promises as fs } from "node:fs";
|
|
|
6
6
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
7
7
|
import { DefaultPackageManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { PackageError, } from "../core/types.js";
|
|
9
|
-
import { BUNDLED } from "../extensions/index.js";
|
|
10
9
|
import { logger } from "../log.js";
|
|
11
10
|
import { defaultAgentDir } from "./config.js";
|
|
12
|
-
import { shadowedBuiltin } from "./pi.js";
|
|
13
11
|
const log = logger("packages");
|
|
14
12
|
/** Set by how long a released fix may sit unnoticed, like update.ts. */
|
|
15
13
|
const CHECK_EVERY_MS = 24 * 60 * 60_000;
|
|
16
14
|
const KINDS = { extensions: "extension", skills: "skill" };
|
|
17
|
-
/** The state line for a skill whose tool this instance is not given (docs/design/03). */
|
|
18
|
-
const FOLLOWS_TOOL = "follows Channels → agent tool";
|
|
19
15
|
/** Written by `rtk init -g --agent pi` (the rtk block's `post_install` hook): a `local` file whose
|
|
20
16
|
* switch is the rtk tool's, so a settings.json pattern would fight the tool. */
|
|
21
17
|
const RTK_FILE = join("extensions", "rtk.ts");
|
|
@@ -154,22 +150,14 @@ export class PiPackageStore {
|
|
|
154
150
|
}
|
|
155
151
|
async #fillPier(pkg) {
|
|
156
152
|
pkg.version = this.pier.version;
|
|
157
|
-
const {
|
|
158
|
-
|
|
159
|
-
pkg.resources = BUNDLED.map(({ name }) => ({
|
|
160
|
-
kind: "extension", name, path: `<inline:${name}>`, enabled: on.includes(name),
|
|
161
|
-
state: on.includes(name) ? shadowedBuiltin(name) : null,
|
|
162
|
-
}));
|
|
163
|
-
const gone = new Set(this.pier.tools.filter((t) => t.skill && !(t.available?.() ?? true)).map((t) => t.skill));
|
|
153
|
+
const { skillsOff: off } = this.pier.settings.get();
|
|
154
|
+
pkg.resources = [];
|
|
164
155
|
for (const dir of this.skillDirs) {
|
|
165
156
|
for (const entry of (await fs.readdir(dir, { withFileTypes: true })).filter((e) => e.isDirectory())) {
|
|
166
157
|
const path = join(dir, entry.name, "SKILL.md");
|
|
167
158
|
if (!existsSync(path))
|
|
168
159
|
continue;
|
|
169
|
-
pkg.resources.push({
|
|
170
|
-
kind: "skill", name: entry.name, path, enabled: !off.includes(entry.name),
|
|
171
|
-
state: gone.has(entry.name) ? FOLLOWS_TOOL : null,
|
|
172
|
-
});
|
|
160
|
+
pkg.resources.push({ kind: "skill", name: entry.name, path, enabled: !off.includes(entry.name), state: null });
|
|
173
161
|
}
|
|
174
162
|
}
|
|
175
163
|
}
|
|
@@ -271,11 +259,7 @@ export class PiPackageStore {
|
|
|
271
259
|
if (resource.locked)
|
|
272
260
|
throw new PackageError("refused", `${resource.name} is ${resource.state} — its switch is under Tools`);
|
|
273
261
|
if (pkg.kind === "pier") {
|
|
274
|
-
|
|
275
|
-
if (kind === "skill")
|
|
276
|
-
this.pier.settings.setSkillsOff(withName(skillsOff, resource.name, !enabled));
|
|
277
|
-
else
|
|
278
|
-
this.pier.settings.setExtensions(withName(extensions, resource.name, enabled));
|
|
262
|
+
this.pier.settings.setSkillsOff(withName(this.pier.settings.get().skillsOff, resource.name, !enabled));
|
|
279
263
|
}
|
|
280
264
|
else {
|
|
281
265
|
const key = kind === "extension" ? "extensions" : "skills";
|
package/dist/agent/pi.js
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
//
|
|
1
|
+
// One of the two files (with packages.ts) allowed to import @earendil-works/pi-*.
|
|
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
5
|
import { basename, dirname, join, sep } from "node:path";
|
|
6
|
-
import { createAgentSession, CredentialSynchronizationError, DefaultResourceLoader,
|
|
7
|
-
import { inlineExtensions } from "../extensions/index.js";
|
|
6
|
+
import { createAgentSession, CredentialSynchronizationError, DefaultResourceLoader, ModelRegistry, ModelRuntime, SessionManager, } from "@earendil-works/pi-coding-agent";
|
|
8
7
|
import { SESSION_TITLE_MAX } from "../core/types.js";
|
|
9
8
|
import { logger } from "../log.js";
|
|
10
9
|
import { textOf, toChatTurns, toSessionEvents, turnMetaAt, } from "./events.js";
|
|
@@ -64,7 +63,7 @@ const TITLE_MAX_TOKENS = 40;
|
|
|
64
63
|
const TITLE_TIMEOUT_MS = 20_000;
|
|
65
64
|
const TITLE_PROMPT = "Name this conversation for a session list. Reply with the title only: at most 12 Chinese characters " +
|
|
66
65
|
"or 6 English words, in the language the user wrote in, no quotes, no trailing period. " +
|
|
67
|
-
"A leading `[name<id> time]` on the user's message is a speaker header, not content.";
|
|
66
|
+
"A leading `[name time]` or `[name<id> time]` on the user's message is a speaker header, not content.";
|
|
68
67
|
/** The halves are labelled so a reply quoting an instruction back is not
|
|
69
68
|
* mistaken for one. */
|
|
70
69
|
const titleRequest = (first, reply) => `${TITLE_PROMPT}\n\n<user>\n${first.slice(0, TITLE_INPUT_CHARS)}\n</user>\n\n<assistant>\n${reply.slice(0, TITLE_INPUT_CHARS)}\n</assistant>`;
|
|
@@ -112,42 +111,6 @@ const bashTimeoutDefault = (pi) => {
|
|
|
112
111
|
}
|
|
113
112
|
});
|
|
114
113
|
};
|
|
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;
|
|
120
|
-
/** Pi loads a bundled extension and its on-disk twin both, leaving two tools
|
|
121
|
-
* of one name; the copy the user put there wins, and the journal says so (§5). */
|
|
122
|
-
export const standDownShadowed = (base) => {
|
|
123
|
-
const inline = (ext) => ext.path.startsWith("<inline:");
|
|
124
|
-
const onDisk = new Map(base.extensions.filter((ext) => !inline(ext)).flatMap((ext) => [...ext.tools.keys()].map((tool) => [tool, ext.path])));
|
|
125
|
-
return {
|
|
126
|
-
...base,
|
|
127
|
-
extensions: base.extensions.filter((ext) => {
|
|
128
|
-
if (!inline(ext))
|
|
129
|
-
return true;
|
|
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}`);
|
|
139
|
-
return false;
|
|
140
|
-
}),
|
|
141
|
-
};
|
|
142
|
-
};
|
|
143
|
-
/** A skill's description is resident in every prompt; one pointing at a tool
|
|
144
|
-
* this session was not given is a route the agent cannot take. */
|
|
145
|
-
export const standDownUndocumented = (tools, skills) => {
|
|
146
|
-
const gone = new Set(tools.filter((tool) => tool.skill && !(tool.available?.() ?? true)).map((tool) => tool.skill));
|
|
147
|
-
if (!gone.size)
|
|
148
|
-
return skills;
|
|
149
|
-
return skills.filter((skill) => !gone.has(skill.name));
|
|
150
|
-
};
|
|
151
114
|
export class PiSession {
|
|
152
115
|
pi;
|
|
153
116
|
pinned;
|
|
@@ -400,7 +363,6 @@ export class PiSession {
|
|
|
400
363
|
}
|
|
401
364
|
}
|
|
402
365
|
export class PiAgentFactory {
|
|
403
|
-
extraTools;
|
|
404
366
|
instructions;
|
|
405
367
|
skillPaths;
|
|
406
368
|
credentials;
|
|
@@ -409,9 +371,9 @@ export class PiAgentFactory {
|
|
|
409
371
|
pier;
|
|
410
372
|
titleModel;
|
|
411
373
|
listings;
|
|
412
|
-
constructor(
|
|
413
|
-
/**
|
|
414
|
-
*
|
|
374
|
+
constructor(
|
|
375
|
+
/** Read per session open, so a Console change reaches the next session
|
|
376
|
+
* without a restart; appended as a context file so the user's own
|
|
415
377
|
* instructions still win. */
|
|
416
378
|
instructions = () => "",
|
|
417
379
|
/** Loaded per session, never installed into the user's skill directories. */
|
|
@@ -419,11 +381,10 @@ export class PiAgentFactory {
|
|
|
419
381
|
/** Optional only for bare test factories; an OAuth refresh persists here,
|
|
420
382
|
* never to a plaintext auth.json. */
|
|
421
383
|
credentials, providerConfig = new PiConfigStore(), pinned = () => [],
|
|
422
|
-
/** The built-in `pier` package's
|
|
423
|
-
pier = () => ({
|
|
384
|
+
/** The built-in `pier` package's one switch list: Pier's own skills switched off. */
|
|
385
|
+
pier = () => ({ skillsOff: [] }), titleModel = () => undefined,
|
|
424
386
|
/** Injected so a test needs no session directory or database. */
|
|
425
387
|
listings = new IndexedListing()) {
|
|
426
|
-
this.extraTools = extraTools;
|
|
427
388
|
this.instructions = instructions;
|
|
428
389
|
this.skillPaths = skillPaths;
|
|
429
390
|
this.credentials = credentials;
|
|
@@ -616,8 +577,14 @@ export class PiAgentFactory {
|
|
|
616
577
|
async logout(providerId) {
|
|
617
578
|
await (await this.authRuntime()).logout(providerId, { signal: AbortSignal.timeout(15_000) });
|
|
618
579
|
}
|
|
580
|
+
/** Pi's registry over the shared runtime, so an OAuth refresh lands in the
|
|
581
|
+
* credential store like every other request's. */
|
|
582
|
+
async webContext(active) {
|
|
583
|
+
const modelRegistry = new ModelRegistry(await this.refreshedRuntime());
|
|
584
|
+
return { modelRegistry, model: active && modelRegistry.find(active.provider, active.id) };
|
|
585
|
+
}
|
|
619
586
|
async resourceLoader(cwd) {
|
|
620
|
-
const {
|
|
587
|
+
const { skillsOff } = this.pier();
|
|
621
588
|
const loader = new DefaultResourceLoader({
|
|
622
589
|
cwd,
|
|
623
590
|
agentDir: defaultAgentDir(),
|
|
@@ -628,13 +595,9 @@ export class PiAgentFactory {
|
|
|
628
595
|
// same name is Pi's to switch (settings.json).
|
|
629
596
|
skillsOverride: (base) => ({
|
|
630
597
|
...base,
|
|
631
|
-
skills:
|
|
598
|
+
skills: base.skills.filter((skill) => !skillsOff.includes(skill.name) || !this.skillPaths.some((dir) => skill.filePath.startsWith(dir + sep))),
|
|
632
599
|
}),
|
|
633
|
-
extensionFactories: [
|
|
634
|
-
{ name: "pier-bash-timeout", factory: bashTimeoutDefault, hidden: true },
|
|
635
|
-
...inlineExtensions(extensions),
|
|
636
|
-
],
|
|
637
|
-
extensionsOverride: standDownShadowed,
|
|
600
|
+
extensionFactories: [{ name: "pier-bash-timeout", factory: bashTimeoutDefault, hidden: true }],
|
|
638
601
|
agentsFilesOverride: (current) => {
|
|
639
602
|
const content = this.instructions();
|
|
640
603
|
return {
|
|
@@ -651,36 +614,6 @@ export class PiAgentFactory {
|
|
|
651
614
|
return this.providerConfig.withWrite(() => this.openSnapshot(cwd, sessionManager, opts));
|
|
652
615
|
}
|
|
653
616
|
async openSnapshot(cwd, sessionManager, opts) {
|
|
654
|
-
let live;
|
|
655
|
-
// Per open: an unconfigured tool would cost context on every turn and answer nothing.
|
|
656
|
-
const active = this.extraTools.filter((tool) => tool.available?.() ?? true);
|
|
657
|
-
const customTools = active.map((tool) => defineTool({
|
|
658
|
-
name: tool.name,
|
|
659
|
-
label: tool.label,
|
|
660
|
-
description: tool.description,
|
|
661
|
-
parameters: tool.parameters,
|
|
662
|
-
execute: async (_id, params, signal) => {
|
|
663
|
-
const caller = live?.sessionId ?? "unknown";
|
|
664
|
-
log.debug(`tool ${tool.name} called by ${caller}`);
|
|
665
|
-
try {
|
|
666
|
-
return {
|
|
667
|
-
content: [
|
|
668
|
-
{
|
|
669
|
-
type: "text",
|
|
670
|
-
// Pretty-printing costs ~20% more tokens and buys the model nothing.
|
|
671
|
-
text: JSON.stringify(await tool.execute(params, caller, signal)),
|
|
672
|
-
},
|
|
673
|
-
],
|
|
674
|
-
details: {},
|
|
675
|
-
};
|
|
676
|
-
}
|
|
677
|
-
catch (err) {
|
|
678
|
-
// Pi turns this into tool-result text: the right recovery, the wrong record.
|
|
679
|
-
log.warn(`tool ${tool.name} failed for ${caller}`, err);
|
|
680
|
-
throw err;
|
|
681
|
-
}
|
|
682
|
-
},
|
|
683
|
-
}));
|
|
684
617
|
// A locked store is a refusal with a reason here, not "provider not
|
|
685
618
|
// configured" later. Before appendSessionInfo, so nothing is written.
|
|
686
619
|
this.credentials?.assertUnlocked();
|
|
@@ -696,11 +629,10 @@ export class PiAgentFactory {
|
|
|
696
629
|
const created = await createAgentSession({
|
|
697
630
|
cwd,
|
|
698
631
|
sessionManager,
|
|
699
|
-
customTools,
|
|
700
632
|
modelRuntime: runtime,
|
|
701
633
|
resourceLoader: await this.resourceLoader(cwd),
|
|
702
634
|
});
|
|
703
|
-
live = created.session;
|
|
635
|
+
const live = created.session;
|
|
704
636
|
// Pi defaults to one follow-up per turn boundary, so N queued messages cost
|
|
705
637
|
// N turns. The agent's setter flips only the in-memory queue;
|
|
706
638
|
// `session.setFollowUpMode` would persist it to Pi's settings.json.
|
package/dist/channels/attach.js
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
import { readFile, stat } from "node:fs/promises";
|
|
6
6
|
import { basename, extname } from "node:path";
|
|
7
7
|
import { lostMarker, replaceOutsideCode } from "../core/inbound-file.js";
|
|
8
|
-
/**
|
|
9
|
-
*
|
|
8
|
+
/** Lark refuses an image past 10 MB, the smaller of the two; one cap so a
|
|
9
|
+
* turn does not land on one chat and not another. */
|
|
10
10
|
export const MAX_ATTACH_BYTES = 10 * 1024 * 1024;
|
|
11
11
|
const MAX_ATTACHMENTS = 5;
|
|
12
12
|
/** svg excluded deliberately: it is markup, and renders as a file. */
|
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
// Slash commands in IM text, parsed once for every platform:
|
|
2
|
-
//
|
|
1
|
+
// Slash commands in IM text, parsed once for every platform: clients add
|
|
2
|
+
// whitespace and capitalise.
|
|
3
|
+
/** The configure-first trigger, one spelling on both platforms: `s <text>`
|
|
4
|
+
* (Lark also `/s <text>`), the text being the panel's pending question. A bare
|
|
5
|
+
* `s` carries no question and is prose. */
|
|
6
|
+
export function settingsDraft(text) {
|
|
7
|
+
return /^\/?s[ \t]+(\S[\s\S]*)$/i.exec(text.trim())?.[1];
|
|
8
|
+
}
|
|
3
9
|
export function parseCommand(text) {
|
|
4
10
|
const trimmed = text.trim();
|
|
5
11
|
if (!trimmed.startsWith("/"))
|
|
6
12
|
return null;
|
|
7
13
|
// Args verbatim: a path or a sentence must keep its own spacing.
|
|
8
14
|
const match = /^\/(\S+)[ \t]*([\s\S]*)$/.exec(trimmed);
|
|
9
|
-
const
|
|
15
|
+
const name = match?.[1] ?? "";
|
|
10
16
|
if (!name)
|
|
11
17
|
return null; // a bare "/" is not a command
|
|
12
|
-
return {
|
|
13
|
-
name: name.toLowerCase(),
|
|
14
|
-
args: match?.[2] ?? "",
|
|
15
|
-
...(target ? { target } : {}),
|
|
16
|
-
};
|
|
18
|
+
return { name: name.toLowerCase(), args: match?.[2] ?? "" };
|
|
17
19
|
}
|
package/dist/channels/config.js
CHANGED
|
@@ -1,23 +1,30 @@
|
|
|
1
1
|
// Channel config persistence and the permission gate every adapter shares: one
|
|
2
2
|
// JSON document per platform, so a surface configuring one reads and writes one row.
|
|
3
3
|
import { randomInt } from "node:crypto";
|
|
4
|
-
import { pierDb } from "../db.js";
|
|
5
|
-
import { isSealed } from "../secrets.js";
|
|
6
4
|
import { defaultChannelConfig, } from "./types.js";
|
|
7
5
|
const BIND_CODE_TTL_MS = 10 * 60_000;
|
|
8
6
|
/** Six symbols out of 36 is 2 billion, but a caller who may retry forever only
|
|
9
7
|
* needs the TTL; five wrong tries void the code instead. */
|
|
10
8
|
const BIND_CODE_TRIES = 5;
|
|
11
9
|
const BIND_CODE_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
10
|
+
/** Where each platform's credentials live in the vault — fixed names, so a
|
|
11
|
+
* skill and the Console agree without a setting. Migration 24 spells the same
|
|
12
|
+
* table in SQL. */
|
|
13
|
+
const CREDENTIAL_NAMES = {
|
|
14
|
+
slack: { token: "SLACK_TOKEN", appToken: "SLACK_APP_TOKEN" },
|
|
15
|
+
lark: { token: "LARK_APP_ID", appToken: "LARK_APP_SECRET" },
|
|
16
|
+
};
|
|
17
|
+
const credentials = (platform) => Object.entries(CREDENTIAL_NAMES[platform]);
|
|
12
18
|
export class ChannelStore {
|
|
13
|
-
secrets;
|
|
14
19
|
db;
|
|
20
|
+
vault;
|
|
15
21
|
cache = new Map();
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
/** The row never holds a credential: `token`/`appToken` are filled from the
|
|
23
|
+
* vault on read and filed there on save. A locked vault throws rather than
|
|
24
|
+
* serving a token it cannot read. */
|
|
25
|
+
constructor(db, vault) {
|
|
20
26
|
this.db = db;
|
|
27
|
+
this.vault = vault;
|
|
21
28
|
}
|
|
22
29
|
/** Private: handed out, a caller could mutate config without saving. */
|
|
23
30
|
cached(platform) {
|
|
@@ -28,11 +35,8 @@ export class ChannelStore {
|
|
|
28
35
|
const config = row
|
|
29
36
|
? { ...defaultChannelConfig(), ...JSON.parse(row.json) }
|
|
30
37
|
: defaultChannelConfig();
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
if (isSealed(config[key]))
|
|
34
|
-
config[key] = this.secrets.decrypt(config[key]);
|
|
35
|
-
}
|
|
38
|
+
for (const [key, name] of credentials(platform)) {
|
|
39
|
+
config[key] = this.vault.get(name) ?? "";
|
|
36
40
|
}
|
|
37
41
|
this.cache.set(platform, config);
|
|
38
42
|
return config;
|
|
@@ -41,16 +45,21 @@ export class ChannelStore {
|
|
|
41
45
|
return structuredClone(this.cached(platform));
|
|
42
46
|
}
|
|
43
47
|
save(platform, config) {
|
|
48
|
+
// The cache holds plaintext; the row holds neither key. Only a changed
|
|
49
|
+
// credential touches the vault, so its `updated` is the rotation, not the
|
|
50
|
+
// last chat discovered; an emptied field removes the row.
|
|
51
|
+
const before = this.cached(platform);
|
|
52
|
+
for (const [key, name] of credentials(platform)) {
|
|
53
|
+
if (config[key] === before[key])
|
|
54
|
+
continue;
|
|
55
|
+
if (config[key])
|
|
56
|
+
this.vault.seal(name, config[key]);
|
|
57
|
+
else
|
|
58
|
+
this.vault.remove(name);
|
|
59
|
+
}
|
|
44
60
|
// Cloned, so the caller's object cannot reach into the cache later.
|
|
45
61
|
this.cache.set(platform, structuredClone(config));
|
|
46
|
-
|
|
47
|
-
const stored = this.secrets ? structuredClone(config) : config;
|
|
48
|
-
if (this.secrets) {
|
|
49
|
-
for (const key of ["token", "appToken"]) {
|
|
50
|
-
if (stored[key])
|
|
51
|
-
stored[key] = this.secrets.encrypt(stored[key]);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
62
|
+
const { token: _token, appToken: _appToken, ...stored } = config;
|
|
54
63
|
this.db.prepare(`
|
|
55
64
|
INSERT INTO channels(platform, json) VALUES (?, ?)
|
|
56
65
|
ON CONFLICT(platform) DO UPDATE SET json = excluded.json
|
|
@@ -59,9 +68,9 @@ export class ChannelStore {
|
|
|
59
68
|
chat(platform, chatId) {
|
|
60
69
|
return this.get(platform).chats.find((c) => c.id === chatId);
|
|
61
70
|
}
|
|
62
|
-
/**
|
|
63
|
-
* on every message: the unchanged case must cost no clone. A new
|
|
64
|
-
* the platform defaults and owns them from then on. */
|
|
71
|
+
/** No platform reliably lists every chat a bot is in, so discovery is passive
|
|
72
|
+
* and happens on every message: the unchanged case must cost no clone. A new
|
|
73
|
+
* chat copies the platform defaults and owns them from then on. */
|
|
65
74
|
discoverChat(platform, chat) {
|
|
66
75
|
const cached = this.cached(platform).chats.find((c) => c.id === chat.id);
|
|
67
76
|
if (cached && cached.name === chat.name && cached.kind === chat.kind)
|
|
@@ -92,7 +101,6 @@ export class ChannelStore {
|
|
|
92
101
|
enabled: true,
|
|
93
102
|
requireMention: config.requireMention,
|
|
94
103
|
requireBind: config.requireBind,
|
|
95
|
-
topicMode: config.topicMode,
|
|
96
104
|
cwd: config.cwd,
|
|
97
105
|
model: config.model,
|
|
98
106
|
thinking: config.thinking,
|
package/dist/channels/control.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// What an adapter may do to a session beyond handing it a prompt. Not part of
|
|
2
2
|
// the `Channel` seam: the channel layer owns the router and hands adapters this
|
|
3
3
|
// narrow interface instead. Thin wrappers over core; no policy.
|
|
4
|
+
import { projectCwds } from "../core/identity.js";
|
|
4
5
|
import { chatOf, isChannelPlatform } from "./types.js";
|
|
5
|
-
export
|
|
6
|
+
export const NO_SESSION = "No session in this thread yet — start one first (Start in the panel).";
|
|
7
|
+
export const HAS_SESSION = "This thread already has a session — send your question as a message.";
|
|
8
|
+
export function createControl({ router, factory, conversations, store, modelMenu }) {
|
|
6
9
|
const launchFor = (key) => {
|
|
7
10
|
if (!isChannelPlatform(key.channelId))
|
|
8
11
|
return {};
|
|
@@ -13,44 +16,87 @@ export function createControl({ router, factory, conversations, store }) {
|
|
|
13
16
|
thinking: policy.thinking ?? undefined,
|
|
14
17
|
};
|
|
15
18
|
};
|
|
19
|
+
/** The thread's session, resumed if evicted; undefined when the thread has
|
|
20
|
+
* none. Never creates: a look at the panel must not open a session. */
|
|
21
|
+
const live = async (key) => {
|
|
22
|
+
if (!conversations.get(key))
|
|
23
|
+
return undefined;
|
|
24
|
+
return router.sessionOf(key) ?? router.ensure(key);
|
|
25
|
+
};
|
|
16
26
|
return {
|
|
17
27
|
launchFor,
|
|
18
28
|
knows: (key) => conversations.get(key) !== undefined,
|
|
19
29
|
abort: (key) => router.abortConversation(key),
|
|
30
|
+
working: (key) => router.sessionOf(key)?.state === "streaming",
|
|
20
31
|
async status(key) {
|
|
21
|
-
const session =
|
|
32
|
+
const session = await live(key);
|
|
22
33
|
if (!session)
|
|
23
34
|
return null;
|
|
24
35
|
const summary = await factory.find(session.id);
|
|
25
36
|
const usage = session.contextUsage;
|
|
26
37
|
return {
|
|
27
38
|
sessionId: session.id,
|
|
28
|
-
|
|
39
|
+
// Not on disk until its first reply; the launch record knows where it is.
|
|
40
|
+
cwd: summary?.cwd ?? conversations.launchOf(key)?.cwd ?? "",
|
|
29
41
|
state: session.state,
|
|
42
|
+
empty: (await session.history()).length === 0,
|
|
30
43
|
model: session.model,
|
|
31
44
|
thinking: session.thinkingLevel,
|
|
32
|
-
thinkingLevels: session.availableThinkingLevels(),
|
|
33
45
|
tokens: usage?.tokens ?? null,
|
|
34
46
|
contextWindow: usage?.contextWindow ?? null,
|
|
35
47
|
};
|
|
36
48
|
},
|
|
37
|
-
|
|
49
|
+
pins: modelMenu,
|
|
38
50
|
async setModel(key, model) {
|
|
39
|
-
await
|
|
51
|
+
const session = await live(key);
|
|
52
|
+
if (!session)
|
|
53
|
+
throw new Error(NO_SESSION);
|
|
54
|
+
await session.setModel(model);
|
|
55
|
+
conversations.amendLaunch(key, { model });
|
|
40
56
|
},
|
|
41
57
|
async setThinking(key, level) {
|
|
42
|
-
|
|
58
|
+
const session = await live(key);
|
|
59
|
+
if (!session)
|
|
60
|
+
throw new Error(NO_SESSION);
|
|
61
|
+
session.setThinkingLevel(level);
|
|
62
|
+
conversations.amendLaunch(key, { thinking: level });
|
|
43
63
|
},
|
|
44
|
-
async newSession(key,
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
async newSession(key, over = {}) {
|
|
65
|
+
const defaults = launchFor(key);
|
|
66
|
+
const launch = { ...defaults, ...over, cwd: over.cwd || defaults.cwd || process.cwd() };
|
|
67
|
+
const session = await factory.create(launch);
|
|
68
|
+
// A message already inside the router's resolve can write the row while Pi
|
|
69
|
+
// was opening: the first row wins, and the session nobody routes to goes.
|
|
70
|
+
if (conversations.get(key)) {
|
|
71
|
+
await session.dispose();
|
|
72
|
+
throw new Error(HAS_SESSION);
|
|
73
|
+
}
|
|
50
74
|
// Persist before attaching: a crash between must not leave an unrecorded session.
|
|
51
|
-
conversations.set(key, session.id);
|
|
75
|
+
conversations.set(key, session.id, launch);
|
|
52
76
|
router.attach(key, session);
|
|
53
77
|
return session.id;
|
|
54
78
|
},
|
|
79
|
+
async recentDirs(key, limit = 6) {
|
|
80
|
+
const own = launchFor(key).cwd;
|
|
81
|
+
// The web's rule (worktrees folded into their project); task runs' directories count too.
|
|
82
|
+
const seen = new Set([...(own ? [own] : []), ...projectCwds(await factory.list())]);
|
|
83
|
+
return [...seen].slice(0, limit);
|
|
84
|
+
},
|
|
85
|
+
async recent(key, exchanges) {
|
|
86
|
+
const session = await live(key);
|
|
87
|
+
if (!session)
|
|
88
|
+
return [];
|
|
89
|
+
const pairs = [];
|
|
90
|
+
for (const turn of await session.history()) {
|
|
91
|
+
const last = pairs.at(-1);
|
|
92
|
+
if (turn.role === "user")
|
|
93
|
+
pairs.push({ user: turn.text });
|
|
94
|
+
// The first reply after a user turn; later assistant turns of the same
|
|
95
|
+
// exchange are continuations of the same answer.
|
|
96
|
+
else if (turn.role === "assistant" && last && last.assistant === undefined)
|
|
97
|
+
last.assistant = turn.text;
|
|
98
|
+
}
|
|
99
|
+
return pairs.slice(-exchanges);
|
|
100
|
+
},
|
|
55
101
|
};
|
|
56
102
|
}
|