@timqi/pier 0.1.14 → 0.3.0

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.
Files changed (130) hide show
  1. package/README.md +44 -22
  2. package/dist/agent/config.js +2 -2
  3. package/dist/agent/events.js +32 -3
  4. package/dist/agent/listing.js +14 -10
  5. package/dist/agent/packages.js +6 -10
  6. package/dist/agent/pi.js +113 -70
  7. package/dist/agent/roles.js +104 -0
  8. package/dist/agent/types.js +78 -0
  9. package/dist/boards/boards.js +42 -32
  10. package/dist/channels/chains.js +0 -4
  11. package/dist/channels/commands.js +7 -12
  12. package/dist/channels/config.js +27 -1
  13. package/dist/channels/control.js +5 -16
  14. package/dist/channels/conversations.js +4 -9
  15. package/dist/channels/lark-api.js +20 -26
  16. package/dist/channels/lark-outbound.js +21 -29
  17. package/dist/channels/lark-panel.js +1 -1
  18. package/dist/channels/lark.js +81 -39
  19. package/dist/channels/lines.js +33 -1
  20. package/dist/channels/panel.js +17 -107
  21. package/dist/channels/routes.js +11 -21
  22. package/dist/channels/runtime.js +79 -25
  23. package/dist/channels/slack-outbound.js +12 -14
  24. package/dist/channels/slack-panel.js +1 -1
  25. package/dist/channels/slack-render.js +1 -1
  26. package/dist/channels/slack-thread.js +65 -0
  27. package/dist/channels/slack.js +85 -124
  28. package/dist/channels/types.js +1 -1
  29. package/dist/cli.js +7 -2
  30. package/dist/config-sync.js +42 -37
  31. package/dist/core/chain.js +221 -0
  32. package/dist/core/identity.js +36 -13
  33. package/dist/core/inbound-file.js +11 -4
  34. package/dist/core/reply.js +111 -81
  35. package/dist/core/router.js +111 -118
  36. package/dist/core/search.js +65 -0
  37. package/dist/core/types.js +38 -77
  38. package/dist/db.js +41 -2
  39. package/dist/drain.js +1 -1
  40. package/dist/main.js +65 -40
  41. package/dist/settings.js +26 -19
  42. package/dist/socket.js +1 -0
  43. package/dist/tasks/agent.js +11 -5
  44. package/dist/tasks/callbacks.js +55 -5
  45. package/dist/tasks/cli.js +22 -11
  46. package/dist/tasks/definitions.js +30 -17
  47. package/dist/tasks/execution.js +11 -5
  48. package/dist/tasks/groups.js +5 -2
  49. package/dist/tasks/messages.js +7 -7
  50. package/dist/tasks/open-items.js +88 -0
  51. package/dist/tasks/operations.js +76 -39
  52. package/dist/tasks/outbox.js +19 -6
  53. package/dist/tasks/routes.js +19 -249
  54. package/dist/tasks/service.js +166 -34
  55. package/dist/tasks/store.js +123 -57
  56. package/dist/tasks/types.js +3 -0
  57. package/dist/tools.js +12 -5
  58. package/dist/web/auth.js +2 -2
  59. package/dist/web/config-sync.js +4 -5
  60. package/dist/web/config.js +9 -2
  61. package/dist/web/explorer.js +2 -2
  62. package/dist/web/fs.js +16 -1
  63. package/dist/web/instance.js +8 -5
  64. package/dist/web/packages.js +1 -1
  65. package/dist/web/passkeys.js +3 -3
  66. package/dist/web/providers.js +3 -3
  67. package/dist/web/public/assets/code-CWT27KH1.js +2 -0
  68. package/dist/web/public/assets/code-CWT27KH1.js.br +0 -0
  69. package/dist/web/public/assets/code-CWT27KH1.js.gz +0 -0
  70. package/dist/web/public/assets/explorer-C4DhXFCm.js +5 -0
  71. package/dist/web/public/assets/explorer-C4DhXFCm.js.br +0 -0
  72. package/dist/web/public/assets/explorer-C4DhXFCm.js.gz +0 -0
  73. package/dist/web/public/assets/highlight-CDSk2hRJ.js +72 -0
  74. package/dist/web/public/assets/highlight-CDSk2hRJ.js.br +0 -0
  75. package/dist/web/public/assets/highlight-CDSk2hRJ.js.gz +0 -0
  76. package/dist/web/public/assets/{hljs-tWqyD59G.js → hljs-BRz6a8Dq.js} +2 -2
  77. package/dist/web/public/assets/hljs-BRz6a8Dq.js.br +0 -0
  78. package/dist/web/public/assets/hljs-BRz6a8Dq.js.gz +0 -0
  79. package/dist/web/public/assets/index-D0ZYdweB.js +14 -0
  80. package/dist/web/public/assets/index-D0ZYdweB.js.br +0 -0
  81. package/dist/web/public/assets/index-D0ZYdweB.js.gz +0 -0
  82. package/dist/web/public/assets/index-JN0Ic8K5.css +2 -0
  83. package/dist/web/public/assets/index-JN0Ic8K5.css.br +0 -0
  84. package/dist/web/public/assets/index-JN0Ic8K5.css.gz +0 -0
  85. package/dist/web/public/assets/settings-DSTeuIyc.js +9 -0
  86. package/dist/web/public/assets/settings-DSTeuIyc.js.br +0 -0
  87. package/dist/web/public/assets/settings-DSTeuIyc.js.gz +0 -0
  88. package/dist/web/public/index.html +39 -109
  89. package/dist/web/public/index.html.br +0 -0
  90. package/dist/web/public/index.html.gz +0 -0
  91. package/dist/web/public/manifest.webmanifest +0 -2
  92. package/dist/web/push.js +3 -3
  93. package/dist/web/server.js +89 -116
  94. package/dist/web/session-state.js +4 -40
  95. package/docs/deploy.md +5 -3
  96. package/package.json +2 -1
  97. package/skills/pier-boards/SKILL.md +14 -4
  98. package/skills/pier-help/SKILL.md +62 -23
  99. package/skills/pier-search/SKILL.md +27 -0
  100. package/skills/pier-tasks/SKILL.md +68 -18
  101. package/dist/channels/handoff.js +0 -94
  102. package/dist/web/public/assets/activity-Ds6fCHnb.js +0 -5
  103. package/dist/web/public/assets/activity-Ds6fCHnb.js.br +0 -0
  104. package/dist/web/public/assets/activity-Ds6fCHnb.js.gz +0 -0
  105. package/dist/web/public/assets/boards-BKCj6EwK.js +0 -1
  106. package/dist/web/public/assets/boards-BKCj6EwK.js.br +0 -0
  107. package/dist/web/public/assets/boards-BKCj6EwK.js.gz +0 -0
  108. package/dist/web/public/assets/explorer-DnTm975c.js +0 -4
  109. package/dist/web/public/assets/explorer-DnTm975c.js.br +0 -0
  110. package/dist/web/public/assets/explorer-DnTm975c.js.gz +0 -0
  111. package/dist/web/public/assets/hljs-tWqyD59G.js.br +0 -0
  112. package/dist/web/public/assets/hljs-tWqyD59G.js.gz +0 -0
  113. package/dist/web/public/assets/index-BQo-haPN.js +0 -85
  114. package/dist/web/public/assets/index-BQo-haPN.js.br +0 -0
  115. package/dist/web/public/assets/index-BQo-haPN.js.gz +0 -0
  116. package/dist/web/public/assets/index-DiHj0w1i.css +0 -2
  117. package/dist/web/public/assets/index-DiHj0w1i.css.br +0 -0
  118. package/dist/web/public/assets/index-DiHj0w1i.css.gz +0 -0
  119. package/dist/web/public/assets/runs-C_AthWcW.js +0 -1
  120. package/dist/web/public/assets/runs-C_AthWcW.js.br +0 -0
  121. package/dist/web/public/assets/runs-C_AthWcW.js.gz +0 -0
  122. package/dist/web/public/assets/settings-DDaAtFlc.js +0 -5
  123. package/dist/web/public/assets/settings-DDaAtFlc.js.br +0 -0
  124. package/dist/web/public/assets/settings-DDaAtFlc.js.gz +0 -0
  125. package/dist/web/public/assets/task-runs-0pvdITiV.js +0 -3
  126. package/dist/web/public/assets/task-runs-0pvdITiV.js.br +0 -0
  127. package/dist/web/public/assets/task-runs-0pvdITiV.js.gz +0 -0
  128. package/dist/web/public/assets/tasks-Bz29caHJ.js +0 -4
  129. package/dist/web/public/assets/tasks-Bz29caHJ.js.br +0 -0
  130. package/dist/web/public/assets/tasks-Bz29caHJ.js.gz +0 -0
package/README.md CHANGED
@@ -9,9 +9,9 @@ something is worth showing.
9
9
  One instance, one account, your own machine. The agent runs shell commands in
10
10
  directories you name — meant for a machine you own, not a shared host.
11
11
 
12
- **Status: pre-release.** The version is `0.0.x`; the database schema is
13
- versioned from `0.0.1` on. Read `docs/deploy.md` before putting it anywhere
14
- reachable.
12
+ **Status: pre-release.** Releases may still change behaviour and the
13
+ database schema between versions; schema upgrades are one-way. Read
14
+ `docs/deploy.md` before putting it anywhere reachable.
15
15
 
16
16
  ## Requirements
17
17
 
@@ -30,15 +30,35 @@ pier serve
30
30
 
31
31
  It listens on `127.0.0.1:3141` (`PORT`, `HOST`) and keeps everything under
32
32
  `~/.pier` (`PIER_HOME`): one SQLite database, the boards it serves, the
33
- `master.key` that seals stored credentials, and the Pi runtime with its
34
- session transcripts (`~/.pier/pi`, unless `PI_CODING_AGENT_DIR` says
35
- otherwise).
33
+ `master.key` that seals stored credentials, the conversation's memory
34
+ (`~/.pier/home`), and the Pi runtime with its session transcripts
35
+ (`~/.pier/pi`, unless `PI_CODING_AGENT_DIR` says otherwise).
36
36
 
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
- `http://localhost:3141`, sign in; **Console → Settings** configures Models,
40
- Agent, Channels, Vault, the public URL, password and master key; **New session**
41
- picks the directory the agent's shell runs in.
39
+ `http://localhost:3141`, sign in; **Console → Settings** has Models, Agent,
40
+ Channels, Vault, Tasks, Boards, Instance (public URL, accent, notifications,
41
+ reload) and Security (master key, password, passkeys, signed-in devices).
42
+
43
+ ## How the conversation works
44
+
45
+ The web chat is one conversation, Pier. Its current session answers,
46
+ remembers and dispatches; it runs in `~/.pier/home`, which holds
47
+ memory only: `MEMORY.md` (one-line facts) and daily notes in
48
+ `memory/YYYY-MM-DD.md`.
49
+
50
+ - Real work is delegated with `pier task run`: a worker for a small task, a
51
+ lead for a feature, which designs with you or builds with its own workers.
52
+ Each is its own session; its result comes back to Pier as a callback.
53
+ - After an idle hour, or past 60K tokens, the next message starts a fresh
54
+ session, seeded with the memory, the open items, recent runs and the last
55
+ exchanges. The old transcripts stay readable.
56
+ - `/status` lists what is open, `/new` starts a fresh session now, `/stop` stops
57
+ the reply in progress, `/skills` lists the skills. `%` works in place of
58
+ `/` (`%status`), because Slack's client eats an unregistered `/`.
59
+ - One IM DM, on Lark or Slack, can be the conversation: Console → Settings →
60
+ Channels, the DM's row, "This DM is the conversation". Pier then
61
+ answers there, from the phone; other chats keep one session per thread.
42
62
 
43
63
  ## Configure Pi
44
64
 
@@ -58,14 +78,15 @@ Console → Settings:
58
78
  - **Models** — endpoints, API-key/OAuth login, pinned models. Credentials are
59
79
  sealed in Pier's SQLite database, never written back to `models.json`.
60
80
  **Test** sends one real request and shows the body as sent and the reply.
61
- - **Agent** — edits `SYSTEM.md`, `AGENTS.md`, `settings.json` and the
62
- `models.json` structure, globally or per project scope (with that project's
63
- `.pi/skills` and `.pi/extensions` listed). Changes apply when a session next
64
- opens; saving recycles idle sessions; **Settings → Instance → Reload** does
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
- managed CLI tools (`rtk`, `rg`, `fd`, `wt`, `jq`, or your own as a
81
+ - **Agent** — edits `SYSTEM.md`, `AGENTS.md` and the `models.json`
82
+ structure, globally or per project scope (with that project's
83
+ `.pi/skills` and `.pi/extensions` listed); `settings.json` is Pier's and
84
+ shown read-only. Changes apply when a session next opens; saving recycles
85
+ idle sessions; **Settings → Instance → Reload** does the same for files
86
+ changed elsewhere. Also here: packages (Pi's registry, **Add package**),
87
+ Pier's own skills as the built-in `pier` package (`pier web search|fetch`
88
+ reaches the web through the provider's hosted tools — Anthropic or OpenAI,
89
+ no key of your own), **Browse files**, and the managed CLI tools (`rtk`, `rg`, `fd`, `wt`, `jq`, or your own as a
69
90
  [ubix](https://github.com/timqi/ubix) block), installed into
70
91
  `~/.pier/tools/bin`, first on every session's PATH beside the `pier` shim
71
92
  Pier writes there at start.
@@ -79,7 +100,7 @@ store and renamed `auth.json.imported`; literal keys in `models.json` likewise
79
100
  (`models.json.imported`). The editor rejects plaintext keys or header values.
80
101
  Pi's provider environment variables are inherited from the Pier process; a
81
102
  systemd service does not inherit your shell, so put non-secret settings in a
82
- unit override and API keys in the Providers UI.
103
+ unit override and API keys in Settings → Models.
83
104
 
84
105
  ## Run it as a service
85
106
 
@@ -118,8 +139,9 @@ npm test # vitest
118
139
 
119
140
  ## Releases
120
141
 
121
- Pier asks `registry.npmjs.org` at boot and every 30 minutes; the footer version
122
- becomes `v0.0.1 → 0.0.2` when a release is out and opens a panel: source link,
142
+ Pier asks `registry.npmjs.org` at boot and every 30 minutes; the version in
143
+ the Settings header turns into a badge when a release is out (`Pier <version>
144
+ is out`) and opens a panel: source link,
123
145
  **Update now**, **Update automatically** (idle instance only). Both drain first
124
146
  and hand the install to the updater unit; off systemd the panel says `pier
125
147
  update`. The updater writes `~/.pier/db/backups/pier.db.release-<version>.bak`
@@ -128,10 +150,10 @@ update`. The updater writes `~/.pier/db/backups/pier.db.release-<version>.bak`
128
150
 
129
151
  `main` is the only development line. `just release [patch|minor|major]` checks,
130
152
  tags and pushes; the tag builds and publishes to npm and a GitHub Release. The
131
- footer version is `package.json`'s.
153
+ version shown is `package.json`'s.
132
154
 
133
155
  ## License
134
156
 
135
157
  [AGPL-3.0-only](LICENSE). Run it, change it, deploy it. If you offer a modified
136
158
  Pier to other people over a network, they are entitled to your source — the
137
- version in the footer links to this repository for exactly that reason.
159
+ version in the Settings header links to this repository for exactly that reason.
@@ -4,7 +4,8 @@
4
4
  import { randomUUID } from "node:crypto";
5
5
  import { promises as fs } from "node:fs";
6
6
  import { join } from "node:path";
7
- import { isProviderApi, isThinkingLevel, validateEndpoint, validateProviderSetup } from "../core/types.js";
7
+ import { isThinkingLevel } from "../core/types.js";
8
+ import { isProviderApi, SNAPSHOT_FILES, validateEndpoint, validateProviderSetup } from "./types.js";
8
9
  import { pierPath } from "../paths.js";
9
10
  import { mergeSnapshotProviders, normalizeAgentSnapshot, snapshotProviders } from "./config-sync.js";
10
11
  const GLOBAL_FILES = ["SYSTEM.md", "AGENTS.md", "settings.json", "models.json"];
@@ -13,7 +14,6 @@ const PROJECT_FILES = ["AGENTS.md"];
13
14
  // go through writeDefaults, and are the only fields a snapshot carries. The
14
15
  // Console shows the file and never edits it; the rest of it is machine-local.
15
16
  const READONLY_FILES = ["settings.json"];
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
18
  * Console installs into, Pi's attribution headers off — a server is not a
19
19
  * person to survey — and a retry budget of minutes, because an unattended
@@ -1,7 +1,7 @@
1
1
  // Pure Pi-event → SessionEventPayload translation. Structurally typed: no Pi
2
2
  // imports, so it is unit-testable without Pi and Pi types never leak past the
3
3
  // seam. The golden table in events.test.ts is the mapping's spec.
4
- import { isThinkingLevel, MAX_STEP_OUTPUT } from "../core/types.js";
4
+ import { isChatCommand, isThinkingLevel, MAX_STEP_OUTPUT } from "../core/types.js";
5
5
  /** An assistant message that calls a tool is work in progress, not a reply. */
6
6
  export const hasToolCalls = (message) => Array.isArray(message?.content) && message.content.some((part) => part.type === "toolCall");
7
7
  export function textOf(content) {
@@ -20,14 +20,36 @@ function systemOrigin(message) {
20
20
  const value = message.details;
21
21
  if (!value || typeof value !== "object")
22
22
  return null;
23
- const { source: raw, ...origin } = value;
23
+ const { source: raw, cwd, ...origin } = value;
24
+ if (origin.kind === "session-seed") {
25
+ const { reason, previousSessionId } = origin;
26
+ return (reason === "first" || reason === "idle" || reason === "lost" || reason === "full" || reason === "new") &&
27
+ (previousSessionId === null || typeof previousSessionId === "string")
28
+ ? { kind: "session-seed", reason, previousSessionId }
29
+ : null;
30
+ }
31
+ if (origin.kind === "chat-command") {
32
+ const { command, sessions } = origin;
33
+ if (!isChatCommand(command))
34
+ return null;
35
+ // A malformed map costs the card its links, not the card.
36
+ const links = sessions && typeof sessions === "object" && !Array.isArray(sessions) && Object.values(sessions).every((v) => typeof v === "string")
37
+ ? { sessions: sessions }
38
+ : {};
39
+ return { kind: "chat-command", command, ...links };
40
+ }
24
41
  if (typeof origin.taskId !== "string" ||
25
42
  typeof origin.runId !== "string" ||
26
43
  (origin.sourceSessionId !== null && typeof origin.sourceSessionId !== "string"))
27
44
  return null;
28
45
  // A half-valid `source` drawn by the card is an `undefined` in a chip.
29
46
  const source = inputSource(raw);
30
- const shape = { ...origin, ...(source ? { source } : {}) };
47
+ // A callback's, and relative paths resolve against it, so only an absolute one is kept.
48
+ const shape = {
49
+ ...origin,
50
+ ...(source ? { source } : {}),
51
+ ...(origin.kind === "task-callback" && typeof cwd === "string" && cwd.startsWith("/") ? { cwd } : {}),
52
+ };
31
53
  if (origin.kind === "task-delegation" || origin.kind === "task-callback") {
32
54
  return shape;
33
55
  }
@@ -168,6 +190,13 @@ export function toChatTurns(messages) {
168
190
  }
169
191
  }
170
192
  const text = textOf(m.content);
193
+ // Only the attempt that ended the turn: one Pi retried past is no failure.
194
+ if (m.role === "assistant" && m.stopReason === "error" && messages[i + 1]?.role !== "assistant") {
195
+ flush("assistant", hasTools ? "" : text, turnMetaAt(messages, i), undefined, m.timestamp);
196
+ turns[turns.length - 1].error = m.errorMessage || "unknown agent error";
197
+ candidate = undefined;
198
+ continue;
199
+ }
171
200
  // Tool-bearing messages are intermediate work, even when they include text.
172
201
  if (!text || hasTools)
173
202
  continue;
@@ -80,16 +80,15 @@ function fold(acc, line, clean) {
80
80
  }
81
81
  /** A picker, not a results page. */
82
82
  const SEARCH_LIMIT = 8;
83
- /** A trigram token is one character, so `snippet()` is asked for its ceiling
84
- * of 64 tokens; the default 12 fits one word. */
83
+ /** Characters of context a snippet keeps around its first match. */
85
84
  const SNIPPET_CHARS = 64;
86
85
  /** Whitespace splits a query: a space is "and this too", not a character to
87
86
  * find. A term is quoted where it has to be, so a phrase is still findable. */
88
87
  const termsOf = (query) => query.split(/\s+/).filter(Boolean);
89
- /** Delimited like `snippet()` (\u0001 … \u0002, `…` for a cut), so a surface
90
- * draws one shape for both paths. Every term is marked where it first
91
- * appears inside the window, as `snippet()` marks every phrase it kept.
92
- * ASCII case folding: LIKE found the row by the same rule. */
88
+ /** Cut here rather than by FTS `snippet()`, which can only cut the indexed
89
+ * text, markup and all. Each match delimited \u0001 … \u0002, `…` for a cut;
90
+ * every term is marked where it first appears inside the window. A match only
91
+ * inside stripped markup leaves the message's opening. */
93
92
  function around(text, terms) {
94
93
  const lower = text.toLowerCase();
95
94
  const found = terms
@@ -98,7 +97,7 @@ function around(text, terms) {
98
97
  .sort((a, b) => a.at - b.at);
99
98
  const first = found[0];
100
99
  if (!first)
101
- return text.slice(0, SNIPPET_CHARS);
100
+ return text.length > SNIPPET_CHARS ? `${text.slice(0, SNIPPET_CHARS)}…` : text;
102
101
  const start = Math.max(0, first.at - SNIPPET_CHARS / 2);
103
102
  const end = Math.min(text.length, first.at + first.length + SNIPPET_CHARS / 2);
104
103
  let out = start ? "…" : "";
@@ -115,14 +114,19 @@ function around(text, terms) {
115
114
  export class IndexedListing {
116
115
  dir;
117
116
  clean;
117
+ plain;
118
118
  #db;
119
119
  #statements;
120
120
  constructor(dir = join(defaultAgentDir(), "sessions"), db,
121
121
  /** Handed in rather than imported: the header rule is core's, and agent/
122
122
  * does not import core at runtime. */
123
- clean = (text) => text) {
123
+ clean = (text) => text,
124
+ /** Chat markup off what was said, before a snippet is cut (core/reply.ts):
125
+ * applied at search time, so rows indexed before it are covered too. */
126
+ plain = (text) => text) {
124
127
  this.dir = dir;
125
128
  this.clean = clean;
129
+ this.plain = plain;
126
130
  this.#db = db;
127
131
  }
128
132
  /** Opened on the first scan: building a factory must not open the database. */
@@ -231,7 +235,7 @@ export class IndexedListing {
231
235
  return [];
232
236
  const rows = terms.every((term) => [...term].length >= 3)
233
237
  // Quoted: a term is a string to find, never FTS syntax.
234
- ? sql(`SELECT session_id, role, at, snippet(session_fts, 0, char(1), char(2), '…', ${SNIPPET_CHARS}) AS snippet
238
+ ? sql(`SELECT session_id, role, at, text
235
239
  FROM session_fts WHERE text MATCH ? ORDER BY bm25(session_fts), at DESC`).iterate(terms.map((term) => `"${term.replaceAll('"', '""')}"`).join(" AND "))
236
240
  : sql(`SELECT session_id, role, at, text FROM session_fts WHERE ${terms.map(() => "text LIKE ? ESCAPE '\\'").join(" AND ")} ORDER BY at DESC`).iterate(...terms.map((term) => `%${term.replaceAll(/[\\%_]/g, "\\$&")}%`));
237
241
  const hits = [];
@@ -245,7 +249,7 @@ export class IndexedListing {
245
249
  sessionId: row.session_id,
246
250
  role: row.role,
247
251
  at: row.at,
248
- snippet: row.snippet ?? around(row.text ?? "", terms),
252
+ snippet: around(this.plain(row.text), terms),
249
253
  });
250
254
  if (hits.length >= limit)
251
255
  break;
@@ -5,17 +5,16 @@
5
5
  import { existsSync, promises as fs } from "node:fs";
6
6
  import { basename, dirname, join, relative, resolve } from "node:path";
7
7
  import { DefaultPackageManager, loadSkills, SettingsManager, } from "@earendil-works/pi-coding-agent";
8
- import { PackageError, } from "../core/types.js";
8
+ import { PackageError, } from "./types.js";
9
9
  import { logger } from "../log.js";
10
10
  import { defaultAgentDir } from "./config.js";
11
11
  const log = logger("packages");
12
12
  /** Set by how long a released fix may sit unnoticed, like update.ts. */
13
13
  const CHECK_EVERY_MS = 24 * 60 * 60_000;
14
14
  const KINDS = { extensions: "extension", skills: "skill" };
15
- /** Written by `rtk init -g --agent pi` (the rtk block's `post_install` hook): a `local` file whose
16
- * switch is the rtk tool's, so a settings.json pattern would fight the tool. */
15
+ /** Written by `rtk init -g --agent pi` (the rtk block's `post_install` hook): the rtk tool's, so it
16
+ * is left out of `local` — the Console switches it as that tool, and a settings.json pattern would fight it. */
17
17
  const RTK_FILE = join("extensions", "rtk.ts");
18
- const RTK_STATE = "installed by the rtk tool";
19
18
  export const kindOf = (source) => source.startsWith("npm:") ? "npm" : /^(git:|(https?|ssh|git):\/\/)/.test(source) ? "git" : "path";
20
19
  /** A trailing `@ref` on an npm or git source; the leading `@` of a scope is not one. */
21
20
  export const pinnedRef = (source) => {
@@ -120,13 +119,12 @@ export class PiPackageStore {
120
119
  row("local", "local", "global", this.agentDir);
121
120
  for (const key of Object.keys(KINDS)) {
122
121
  for (const r of resolved[key]) {
122
+ if (r.path === rtkPath)
123
+ continue;
123
124
  const own = r.metadata.origin === "top-level";
124
125
  const scope = r.metadata.scope === "project" ? "project" : "global";
125
- const resource = resourceRow(KINDS[key], r);
126
- if (r.path === rtkPath)
127
- Object.assign(resource, { state: RTK_STATE, locked: true });
128
126
  row(own ? "local" : r.metadata.source, own ? "local" : kindOf(r.metadata.source), scope, r.metadata.baseDir ?? null)
129
- .resources.push(resource);
127
+ .resources.push(resourceRow(KINDS[key], r));
130
128
  }
131
129
  }
132
130
  // Configured but unresolved (not installed, or empty): still a row.
@@ -277,8 +275,6 @@ export class PiPackageStore {
277
275
  async setEnabled(change) {
278
276
  const { source, kind, path, enabled, cwd } = change;
279
277
  const { pkg, resource } = await this.#find(change);
280
- if (resource.locked)
281
- throw new PackageError("refused", `${resource.name} is ${resource.state} — its switch is under Tools`);
282
278
  if (pkg.kind === "pier") {
283
279
  this.pier.settings.setSkillsOff(withName(this.pier.settings.get().skillsOff, resource.name, !enabled));
284
280
  }