@llblab/pi-kit 0.22.1 → 0.23.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 (87) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +4 -4
  3. package/node_modules/@llblab/pi-grow-loop/AGENTS.md +10 -6
  4. package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +7 -0
  5. package/node_modules/@llblab/pi-grow-loop/README.md +2 -0
  6. package/node_modules/@llblab/pi-grow-loop/dist/index.d.ts +33 -0
  7. package/node_modules/@llblab/pi-grow-loop/dist/index.js +286 -0
  8. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.d.ts +1 -0
  9. package/node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js +1 -0
  10. package/node_modules/@llblab/pi-grow-loop/dist/skills/grow-loop/SKILL.md +117 -0
  11. package/node_modules/@llblab/pi-grow-loop/dist/skills/while-true/SKILL.md +233 -0
  12. package/node_modules/@llblab/pi-grow-loop/index.ts +67 -12
  13. package/node_modules/@llblab/pi-grow-loop/package.json +9 -8
  14. package/node_modules/@llblab/pi-state-flow/AGENTS.md +23 -17
  15. package/node_modules/@llblab/pi-state-flow/BACKLOG.md +5 -3
  16. package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +11 -0
  17. package/node_modules/@llblab/pi-state-flow/README.md +18 -6
  18. package/node_modules/@llblab/pi-state-flow/dist/index.d.ts +2 -1
  19. package/node_modules/@llblab/pi-state-flow/dist/index.js +1 -0
  20. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.d.ts +2 -2
  21. package/node_modules/@llblab/pi-state-flow/dist/lib/artifact.js +5 -5
  22. package/node_modules/@llblab/pi-state-flow/dist/lib/context.d.ts +3 -2
  23. package/node_modules/@llblab/pi-state-flow/dist/lib/context.js +7 -2
  24. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.d.ts +5 -5
  25. package/node_modules/@llblab/pi-state-flow/dist/lib/continuation.js +54 -40
  26. package/node_modules/@llblab/pi-state-flow/dist/lib/durable.js +20 -20
  27. package/node_modules/@llblab/pi-state-flow/dist/lib/extension.js +755 -325
  28. package/node_modules/@llblab/pi-state-flow/dist/lib/git.d.ts +2 -2
  29. package/node_modules/@llblab/pi-state-flow/dist/lib/git.js +14 -17
  30. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.d.ts +4 -0
  31. package/node_modules/@llblab/pi-state-flow/dist/lib/protocol.js +65 -23
  32. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.d.ts +16 -5
  33. package/node_modules/@llblab/pi-state-flow/dist/lib/recovery.js +32 -15
  34. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.d.ts +28 -2
  35. package/node_modules/@llblab/pi-state-flow/dist/lib/runtime.js +276 -26
  36. package/node_modules/@llblab/pi-state-flow/dist/lib/session.d.ts +5 -0
  37. package/node_modules/@llblab/pi-state-flow/dist/lib/session.js +36 -2
  38. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.d.ts +3 -0
  39. package/node_modules/@llblab/pi-state-flow/dist/lib/snapshot.js +3 -0
  40. package/node_modules/@llblab/pi-state-flow/dist/lib/status.d.ts +1 -0
  41. package/node_modules/@llblab/pi-state-flow/dist/lib/status.js +3 -1
  42. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.d.ts +11 -0
  43. package/node_modules/@llblab/pi-state-flow/dist/lib/storage.js +150 -24
  44. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.d.ts +14 -1
  45. package/node_modules/@llblab/pi-state-flow/dist/lib/telegram.js +52 -18
  46. package/node_modules/@llblab/pi-state-flow/dist/lib/transition.js +8 -8
  47. package/node_modules/@llblab/pi-state-flow/dist/package.json +1 -1
  48. package/node_modules/@llblab/pi-state-flow/dist/skills/state-flow-guide/SKILL.md +3 -1
  49. package/node_modules/@llblab/pi-state-flow/docs/architecture.md +53 -7
  50. package/node_modules/@llblab/pi-state-flow/docs/compatibility.md +84 -44
  51. package/node_modules/@llblab/pi-state-flow/docs/filesystem-recovery.md +12 -2
  52. package/node_modules/@llblab/pi-state-flow/docs/fork-contract.md +6 -4
  53. package/node_modules/@llblab/pi-state-flow/docs/performance.md +2 -2
  54. package/node_modules/@llblab/pi-state-flow/docs/temporal-acceptance.md +28 -8
  55. package/node_modules/@llblab/pi-state-flow/docs/usage.md +41 -14
  56. package/node_modules/@llblab/pi-state-flow/index.ts +3 -0
  57. package/node_modules/@llblab/pi-state-flow/lib/artifact.ts +5 -5
  58. package/node_modules/@llblab/pi-state-flow/lib/context.ts +8 -3
  59. package/node_modules/@llblab/pi-state-flow/lib/continuation.ts +57 -40
  60. package/node_modules/@llblab/pi-state-flow/lib/durable.ts +20 -20
  61. package/node_modules/@llblab/pi-state-flow/lib/extension.ts +719 -316
  62. package/node_modules/@llblab/pi-state-flow/lib/git.ts +16 -18
  63. package/node_modules/@llblab/pi-state-flow/lib/protocol.ts +60 -24
  64. package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +34 -21
  65. package/node_modules/@llblab/pi-state-flow/lib/runtime.ts +290 -25
  66. package/node_modules/@llblab/pi-state-flow/lib/session.ts +37 -2
  67. package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +3 -0
  68. package/node_modules/@llblab/pi-state-flow/lib/status.ts +4 -1
  69. package/node_modules/@llblab/pi-state-flow/lib/storage.ts +141 -22
  70. package/node_modules/@llblab/pi-state-flow/lib/telegram.ts +60 -19
  71. package/node_modules/@llblab/pi-state-flow/lib/transition.ts +8 -8
  72. package/node_modules/@llblab/pi-state-flow/package.json +1 -1
  73. package/node_modules/@llblab/pi-state-flow/skills/state-flow-guide/SKILL.md +3 -1
  74. package/node_modules/@llblab/pi-telegram/AGENTS.md +3 -2
  75. package/node_modules/@llblab/pi-telegram/CHANGELOG.md +10 -0
  76. package/node_modules/@llblab/pi-telegram/README.md +3 -1
  77. package/node_modules/@llblab/pi-telegram/dist/lib/bus-leader.js +22 -3
  78. package/node_modules/@llblab/pi-telegram/dist/lib/skills.d.ts +8 -2
  79. package/node_modules/@llblab/pi-telegram/dist/lib/skills.js +36 -4
  80. package/node_modules/@llblab/pi-telegram/dist/package.json +3 -8
  81. package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +1 -1
  82. package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
  83. package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +27 -3
  84. package/node_modules/@llblab/pi-telegram/lib/skills.ts +49 -5
  85. package/node_modules/@llblab/pi-telegram/package.json +3 -8
  86. package/node_modules/@llblab/pi-telegram/scripts/build-dist.mjs +103 -32
  87. package/package.json +6 -6
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.2",
3
+ "version": "0.51.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,12 +31,13 @@
31
31
  "test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
32
32
  "typecheck": "tsc --noEmit",
33
33
  "build": "node scripts/build-dist.mjs",
34
+ "build:check": "node scripts/build-dist.mjs --check",
34
35
  "prepack": "npm run build",
35
36
  "check": "node -e \"await import('./dist/pi-telegram/index.js'); console.log('pi-telegram: extension import ok')\"",
36
37
  "audit": "npm audit --omit=peer",
37
38
  "audit:host": "npm audit",
38
39
  "pack:check": "npm pack --dry-run",
39
- "validate": "npm run build && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
40
+ "validate": "npm run build:check && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
40
41
  },
41
42
  "files": [
42
43
  "index.ts",
@@ -102,15 +103,9 @@
102
103
  "extensions": [
103
104
  "./dist/pi-telegram/index.js"
104
105
  ],
105
- "sourceExtensions": [
106
- "./index.ts"
107
- ],
108
106
  "skills": [
109
107
  "./dist/skills"
110
108
  ],
111
- "sourceSkills": [
112
- "./skills"
113
- ],
114
109
  "image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
115
110
  },
116
111
  "peerDependencies": {
@@ -151,7 +151,7 @@ A registered instance exposes:
151
151
 
152
152
  ## Approved Next Contract: Directory Names And Reclaimable Slots
153
153
 
154
- Status: approved design with a locally tested pure selection policy in `lib/workspace-slots.ts` and profile-isolated display preference persistence/default resolution in `lib/config.ts`. Workspace claims now reserve global letters before provisioning and preserve legacy binding keys. An exact claim assigns the first free letter to a missing-slot binding or the selected member of a duplicate-slot set, but persistence waits for successful target recovery; unresolved duplicates block unrelated fresh allocation. Sticky suffix metadata and acknowledged `displayTitle` persist in Workspace bindings. `lib/thread-display.ts` provides the three-mode projection plus serialized title reconciliation wired into leader startup and follower registration. Heartbeat ACKs carry acknowledged display titles to followers and the current-thread/TUI projection uses them without changing restoration identity. Settings now exposes Letters (default), Names, and Directories; follower changes use the capability-gated leader-owned setting path. Live bot chooser/notice labels and cross-instance agent-target resolution use acknowledged titles without granting routing authority. The Thread store now persists the first proven `inactiveSinceMs` transition with exact confirmed target absence or fenced non-destructive detachment of a confirmed-dead follower or quiescent quitting leader whose tab is preserved; successful active provisioning clears it. Pressure selection, intents, mocked execution, and recovery are implemented. A 2/2 same-model independent post-fix quorum cleared the admission-composition blocker at 0.96 confidence per reviewer, and the operator has since authorized demand-driven rotation. Fresh allocation now invokes the existing retirement lifecycle under full capacity; `BACKLOG.md` owns remaining disposable-client acceptance.
154
+ Status: approved design with a locally tested pure selection policy in `lib/workspace-slots.ts` and profile-isolated display preference persistence/default resolution in `lib/config.ts`. Workspace claims now reserve global letters before provisioning and preserve legacy binding keys. An exact claim assigns the first free letter to a missing-slot binding or the selected member of a duplicate-slot set, but persistence waits for successful target recovery; unresolved duplicates block unrelated fresh allocation. If authenticated follower re-registration finds that its retained target record carries another letter, the exact claim is canonical: registration repairs that record before binding commit and leaves the unrelated binding that owns the stale letter intact. Sticky suffix metadata and acknowledged `displayTitle` persist in Workspace bindings. `lib/thread-display.ts` provides the three-mode projection plus serialized title reconciliation wired into leader startup and follower registration. Heartbeat ACKs carry acknowledged display titles to followers and the current-thread/TUI projection uses them without changing restoration identity. Settings now exposes Letters (default), Names, and Directories; follower changes use the capability-gated leader-owned setting path. Live bot chooser/notice labels and cross-instance agent-target resolution use acknowledged titles without granting routing authority. The Thread store now persists the first proven `inactiveSinceMs` transition with exact confirmed target absence or fenced non-destructive detachment of a confirmed-dead follower or quiescent quitting leader whose tab is preserved; successful active provisioning clears it. Pressure selection, intents, mocked execution, and recovery are implemented. A 2/2 same-model independent post-fix quorum cleared the admission-composition blocker at 0.96 confidence per reviewer, and the operator has since authorized demand-driven rotation. Fresh allocation now invokes the existing retirement lifecycle under full capacity; `BACKLOG.md` owns remaining disposable-client acceptance.
155
155
 
156
156
  The pure policy distinguishes a free letter, a proposed pressure-reclamation victim, and protected/invalid capacity. Its caller must supply a validated profile-wide snapshot, reservations, proven inactivity start, and explicit protection classification; duplicate legacy letters block selection. The policy performs no filesystem or Telegram operations and does not establish liveness or deletion authority. It proposes a victim only when every profile-wide letter is occupied or reserved; elapsed time alone never triggers retirement.
157
157
 
@@ -71,7 +71,7 @@ Every assistant-authored HTML comment is transport-private on Telegram: previews
71
71
  - `telegram_channel_post(action, operation_id, markdown?)` edits or deletes one exact `published` record returned by `telegram_channel_posts`. Edit requires Markdown and delete forbids it. A media-post edit replaces the caption through `editMessageCaption`, while a text post uses `editMessageText`; both render Markdown formatting, including spoilers, as Telegram HTML. The direct leader fences the tool call as outcome-unknown before the mutation call, so ambiguous failures are never replayed automatically.
72
72
  - `telegram_channel_posts(chat_id?, limit?)` lists newest bounded records from the active profile's agent-owned post journal. It returns publication, edit/delete outcome-unknown, confirmed, and deleted local records only, including retained media kind/file name/size/SHA-256 identity; it never reads or claims completeness for Telegram channel history. This explicit successful listing is the only tool response that exposes retained authored Markdown; channel tool failures use fixed redacted messages, while pre-issuance media/caption validation errors stay actionable.
73
73
  - `telegram_message(text, chat_id?, media?, channel?, thread_id?, thread?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. A public `@username`, or an exact negative numeric channel ID with `channel: true`, is passed as `chat_id` without a local registry; channel delivery requires the direct leader, and Telegram enforces whether the bot has channel posting permission. Channel delivery accepts `media` as one local `.jpg`/`.jpeg`/`.png`/`.webp` photo or `.mp4` video (photo ≤ 10 MiB, video ≤ 50 MiB), uploaded through multipart `sendPhoto`/`sendVideo` with `text` as its HTML caption (≤ 1024 visible characters); unsupported media types and albums are rejected before issuance, and the durable channel-post journal binds media identity and caption so duplicate or lost-acknowledgement retries never re-upload. `thread` accepts a live numeric Thread id or its current acknowledged display title; name matching is case-insensitive and fails closed when absent or ambiguous, while delivery captures the numeric target. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed. Outside active turns, paired/default local/TUI delivery remains unchanged. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
74
- - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The bundled `show-me` Skill owns portable evidence-honest explanations and adapts them to phone-width Markdown or self-contained HTML artifacts when Telegram is the active surface. The regular prompt routes applicable turns to these and the other bundled Skills. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
74
+ - The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The bundled `show-me` Skill owns portable evidence-honest explanations and adapts them to phone-width Markdown or self-contained HTML artifacts when Telegram is the active surface. The regular prompt routes applicable turns to these and the other bundled Skills. Compiled npm/git installations expose them through the package `pi.skills` manifest so Pi resource filters remain authoritative; a raw TypeScript checkout under Pi's `extensions` directory contributes its source Skill root at runtime instead. `telegram_attach`, `telegram_bind`, and `telegram_message` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
75
75
  - `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
76
76
  - `telegram_button` hidden comments create footer buttons; standalone column-zero triple-backtick `telegram_button` blocks create button rows between paragraphs in Native Rich Markdown. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing; fenced blocks also accept adjacent top-level JSON/CML objects without an outer array or commas as vertical singleton rows. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero action wrappers, outside quotes, lists, or enclosing code examples. Ordinary code fences and larger outer fences preserve literal examples; bare JSON/CML in prose never activates.
77
77
 
@@ -1210,13 +1210,36 @@ export function createTelegramBusFollowerTargetProvisioner(
1210
1210
  : recoverableTarget && !pendingTargetRecovery
1211
1211
  ? await recoverRequestedTarget()
1212
1212
  : await provisionTarget();
1213
+ const alignResultWithWorkspaceSlot = (): void => {
1214
+ if (
1215
+ !workspaceIdentity ||
1216
+ result.record.slot === workspaceIdentity.slot
1217
+ ) return;
1218
+ deps.recordRuntimeEvent(
1219
+ "bus",
1220
+ "Telegram follower record slot reconciled to its Workspace claim",
1221
+ {
1222
+ phase: "follower-register-slot-reconcile",
1223
+ instanceId: registration.instanceId,
1224
+ chatId: result.target.chatId,
1225
+ threadId: result.target.threadId,
1226
+ previousSlot: result.record.slot,
1227
+ slot: workspaceIdentity.slot,
1228
+ },
1229
+ );
1230
+ result = {
1231
+ ...result,
1232
+ record: { ...result.record, slot: workspaceIdentity.slot },
1233
+ };
1234
+ };
1235
+ alignResultWithWorkspaceSlot();
1213
1236
  const crossSessionReuse =
1214
1237
  !!reconnectRecord &&
1215
1238
  reconnectRecord.instanceId !== registration.instanceId;
1216
1239
  if (reconnectRecord && !crossSessionReuse) {
1217
1240
  const nowMs = getNowMs();
1218
1241
  const refreshedRecord = deps.topicTargetStore.upsert({
1219
- ...reconnectRecord,
1242
+ ...result.record,
1220
1243
  instanceId: registration.instanceId,
1221
1244
  updatedAtMs: nowMs,
1222
1245
  lastSyncObservedAtMs: nowMs,
@@ -1300,7 +1323,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1300
1323
  } else if (crossSessionReuse && reconnectRecord) {
1301
1324
  const nowMs = getNowMs();
1302
1325
  const transferredRecord = deps.topicTargetStore.upsert({
1303
- ...reconnectRecord,
1326
+ ...result.record,
1304
1327
  profileKey: followerProfileKey,
1305
1328
  owner:
1306
1329
  followerOwner.kind === "manual-follower"
@@ -1361,6 +1384,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1361
1384
  }
1362
1385
  }
1363
1386
  if (workspaceIdentity) {
1387
+ alignResultWithWorkspaceSlot();
1364
1388
  const workspaceCommit =
1365
1389
  Threads.commitTelegramWorkspaceProvisionBinding({
1366
1390
  store: deps.topicTargetStore,
@@ -1373,7 +1397,7 @@ export function createTelegramBusFollowerTargetProvisioner(
1373
1397
  ...(result.record.threadName
1374
1398
  ? { threadName: result.record.threadName }
1375
1399
  : {}),
1376
- ...(result.record.slot ? { slot: result.record.slot } : {}),
1400
+ slot: workspaceIdentity.slot,
1377
1401
  journalBindingKeys: [followerProfileKey],
1378
1402
  journalBindingsComplete: true,
1379
1403
  updatedAtMs: getNowMs(),
@@ -1,21 +1,65 @@
1
1
  /**
2
2
  * Bundled Telegram skill discovery
3
3
  * Zones: pi agent, telegram guidance
4
- * Owns source-checkout and installed-package skill path contribution
4
+ * Owns source-checkout skill contribution; installed packages use their manifest
5
5
  */
6
6
 
7
+ import { existsSync } from "node:fs";
8
+ import { basename, dirname, join, resolve } from "node:path";
7
9
  import { fileURLToPath } from "node:url";
8
10
 
9
11
  import type { ExtensionAPI } from "./pi.ts";
12
+ import { resolveAgentDir } from "./paths.ts";
10
13
 
11
- export const TELEGRAM_SKILLS_PATH = fileURLToPath(
12
- new URL("../skills", import.meta.url),
14
+ const TELEGRAM_SKILLS_MODULE_PATH = fileURLToPath(import.meta.url);
15
+
16
+ export function getTelegramExtensionPackageRoot(modulePath: string): string {
17
+ let current = dirname(modulePath);
18
+ while (true) {
19
+ if (existsSync(join(current, "package.json"))) {
20
+ const parent = dirname(current);
21
+ if (basename(current) === "dist" && existsSync(join(parent, "package.json"))) {
22
+ return parent;
23
+ }
24
+ return current;
25
+ }
26
+ const parent = dirname(current);
27
+ if (parent === current) return dirname(modulePath);
28
+ current = parent;
29
+ }
30
+ }
31
+
32
+ export interface TelegramRawExtensionCheckoutOptions {
33
+ agentDir?: string;
34
+ cwd?: string;
35
+ }
36
+
37
+ export function isRawTelegramExtensionCheckout(
38
+ modulePath: string,
39
+ options: TelegramRawExtensionCheckoutOptions = {},
40
+ ): boolean {
41
+ const packageRoot = resolve(getTelegramExtensionPackageRoot(modulePath));
42
+ const agentDir = resolve(options.agentDir ?? resolveAgentDir());
43
+ const cwd = resolve(options.cwd ?? process.cwd());
44
+ return dirname(packageRoot) === join(agentDir, "extensions") ||
45
+ dirname(packageRoot) === join(cwd, ".pi", "extensions");
46
+ }
47
+
48
+ export const TELEGRAM_SKILLS_PATH = join(
49
+ getTelegramExtensionPackageRoot(TELEGRAM_SKILLS_MODULE_PATH),
50
+ "skills",
13
51
  );
14
52
 
15
53
  export function registerTelegramSkillDiscovery(
16
54
  pi: Pick<ExtensionAPI, "on">,
17
- ): void {
55
+ modulePath = TELEGRAM_SKILLS_MODULE_PATH,
56
+ options: TelegramRawExtensionCheckoutOptions = {},
57
+ ): boolean {
58
+ // Pi auto-discovers extension entrypoints but not their manifest Skills.
59
+ // Manifest-loaded packages own Skills and filters through `pi.skills`.
60
+ if (!isRawTelegramExtensionCheckout(modulePath, options)) return false;
18
61
  pi.on("resources_discover", () => ({
19
- skillPaths: [TELEGRAM_SKILLS_PATH],
62
+ skillPaths: [join(getTelegramExtensionPackageRoot(modulePath), "skills")],
20
63
  }));
64
+ return true;
21
65
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.2",
3
+ "version": "0.51.4",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -31,12 +31,13 @@
31
31
  "test:verbose": "node --experimental-strip-types --test --test-reporter=spec tests/*.test.ts",
32
32
  "typecheck": "tsc --noEmit",
33
33
  "build": "node scripts/build-dist.mjs",
34
+ "build:check": "node scripts/build-dist.mjs --check",
34
35
  "prepack": "npm run build",
35
36
  "check": "node -e \"await import('./dist/pi-telegram/index.js'); console.log('pi-telegram: extension import ok')\"",
36
37
  "audit": "npm audit --omit=peer",
37
38
  "audit:host": "npm audit",
38
39
  "pack:check": "npm pack --dry-run",
39
- "validate": "npm run build && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
40
+ "validate": "npm run build:check && npm run typecheck && npm test && npm run audit && npm run check && npm run pack:check"
40
41
  },
41
42
  "files": [
42
43
  "index.ts",
@@ -102,15 +103,9 @@
102
103
  "extensions": [
103
104
  "./dist/pi-telegram/index.js"
104
105
  ],
105
- "sourceExtensions": [
106
- "./index.ts"
107
- ],
108
106
  "skills": [
109
107
  "./dist/skills"
110
108
  ],
111
- "sourceSkills": [
112
- "./skills"
113
- ],
114
109
  "image": "https://raw.githubusercontent.com/llblab/pi-telegram/main/screenshot.png"
115
110
  },
116
111
  "peerDependencies": {
@@ -1,44 +1,115 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Builds the distributive JavaScript, declarations, Pi entrypoint, and Skills.
5
- * Usage: node scripts/build-dist.mjs
6
- *
7
- * The build replaces `dist`, compiles with `tsconfig.build.json`, then copies
8
- * package-owned runtime assets. It does not publish, pack, or modify sources.
4
+ * Builds or verifies the distributive JavaScript, declarations, Pi entrypoint,
5
+ * Skills, and runtime assets without exposing a partial tree.
9
6
  */
10
7
 
11
8
  import { spawnSync } from "node:child_process";
12
- import { cpSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
13
- import { join } from "node:path";
9
+ import {
10
+ cpSync,
11
+ existsSync,
12
+ mkdirSync,
13
+ mkdtempSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ renameSync,
17
+ rmSync,
18
+ writeFileSync,
19
+ } from "node:fs";
20
+ import { join, relative } from "node:path";
21
+
22
+ const DIST_DIR = "dist";
23
+ const checkOnly = process.argv.includes("--check");
14
24
 
15
25
  function run(command, args) {
16
26
  const result = spawnSync(command, args, { stdio: "inherit" });
17
27
  if (result.error) throw result.error;
18
- if (result.status !== 0) process.exit(result.status ?? 1);
28
+ if (result.status !== 0) {
29
+ throw new Error(`${command} exited with status ${result.status ?? "unknown"}.`);
30
+ }
31
+ }
32
+
33
+ function listFiles(root, current = root) {
34
+ return readdirSync(current, { withFileTypes: true }).flatMap((entry) => {
35
+ const path = join(current, entry.name);
36
+ return entry.isDirectory() ? listFiles(root, path) : [relative(root, path)];
37
+ }).sort();
38
+ }
39
+
40
+ function normalizeTextFiles(root) {
41
+ const textSuffixes = [".js", ".ts", ".mjs", ".json", ".md"];
42
+ for (const path of listFiles(root)) {
43
+ if (!textSuffixes.some((suffix) => path.endsWith(suffix))) continue;
44
+ const absolutePath = join(root, path);
45
+ const source = readFileSync(absolutePath, "utf8");
46
+ const normalized = source.replace(/\r\n?/g, "\n");
47
+ if (normalized !== source) writeFileSync(absolutePath, normalized, "utf8");
48
+ }
49
+ }
50
+
51
+ function assertTreesEqual(expectedRoot, actualRoot) {
52
+ if (!existsSync(expectedRoot)) {
53
+ throw new Error(`${expectedRoot} is missing; run npm run build.`);
54
+ }
55
+ const expectedFiles = listFiles(expectedRoot);
56
+ const actualFiles = listFiles(actualRoot);
57
+ if (JSON.stringify(expectedFiles) !== JSON.stringify(actualFiles)) {
58
+ throw new Error("dist file inventory is stale; run npm run build.");
59
+ }
60
+ for (const path of expectedFiles) {
61
+ if (!readFileSync(join(expectedRoot, path)).equals(readFileSync(join(actualRoot, path)))) {
62
+ throw new Error(`dist/${path} is stale; run npm run build.`);
63
+ }
64
+ }
65
+ }
66
+
67
+ function replaceDist(candidate) {
68
+ const backup = `.dist-backup-${process.pid}-${Date.now()}`;
69
+ const hadDist = existsSync(DIST_DIR);
70
+ if (hadDist) renameSync(DIST_DIR, backup);
71
+ try {
72
+ renameSync(candidate, DIST_DIR);
73
+ if (hadDist) rmSync(backup, { recursive: true, force: true });
74
+ } catch (error) {
75
+ if (hadDist && existsSync(backup) && !existsSync(DIST_DIR)) {
76
+ renameSync(backup, DIST_DIR);
77
+ }
78
+ throw error;
79
+ }
19
80
  }
20
81
 
21
- rmSync("dist", { recursive: true, force: true });
22
- mkdirSync("dist", { recursive: true });
23
-
24
- run(process.execPath, [
25
- join("node_modules", "typescript", "bin", "tsc"),
26
- "-p",
27
- "tsconfig.build.json",
28
- ]);
29
-
30
- mkdirSync(join("dist", "pi-telegram"), { recursive: true });
31
- writeFileSync(
32
- join("dist", "pi-telegram", "index.js"),
33
- 'export { default } from "../index.js";\n',
34
- "utf8",
35
- );
36
-
37
- cpSync("skills", join("dist", "skills"), { recursive: true });
38
- cpSync("package.json", join("dist", "package.json"));
39
- cpSync(
40
- join("lib", "generative-app-worker.mjs"),
41
- join("dist", "lib", "generative-app-worker.mjs"),
42
- );
43
-
44
- run(process.execPath, ["--check", join("dist", "pi-telegram", "index.js")]);
82
+ const candidate = mkdtempSync(join(process.cwd(), ".dist-build-"));
83
+ try {
84
+ run(process.execPath, [
85
+ join("node_modules", "typescript", "bin", "tsc"),
86
+ "-p",
87
+ "tsconfig.build.json",
88
+ "--outDir",
89
+ candidate,
90
+ ]);
91
+
92
+ mkdirSync(join(candidate, "pi-telegram"), { recursive: true });
93
+ writeFileSync(
94
+ join(candidate, "pi-telegram", "index.js"),
95
+ 'export { default } from "../index.js";\n',
96
+ "utf8",
97
+ );
98
+ normalizeTextFiles(candidate);
99
+ cpSync("skills", join(candidate, "skills"), { recursive: true });
100
+ cpSync("package.json", join(candidate, "package.json"));
101
+ cpSync(
102
+ join("lib", "generative-app-worker.mjs"),
103
+ join(candidate, "lib", "generative-app-worker.mjs"),
104
+ );
105
+ run(process.execPath, ["--check", join(candidate, "pi-telegram", "index.js")]);
106
+
107
+ if (checkOnly) {
108
+ assertTreesEqual(DIST_DIR, candidate);
109
+ console.log("pi-telegram: dist is current");
110
+ } else {
111
+ replaceDist(candidate);
112
+ }
113
+ } finally {
114
+ rmSync(candidate, { recursive: true, force: true });
115
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-kit",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -43,9 +43,9 @@
43
43
  "@llblab/pi-actors": "0.53.0",
44
44
  "@llblab/pi-clean-room": "0.2.0",
45
45
  "@llblab/pi-codex-usage": "0.10.0",
46
- "@llblab/pi-grow-loop": "0.8.1",
47
- "@llblab/pi-state-flow": "0.18.1",
48
- "@llblab/pi-telegram": "0.51.2",
46
+ "@llblab/pi-grow-loop": "0.8.2",
47
+ "@llblab/pi-state-flow": "0.19.0",
48
+ "@llblab/pi-telegram": "0.51.4",
49
49
  "@llblab/skills": "1.15.0"
50
50
  },
51
51
  "bundledDependencies": [
@@ -62,13 +62,13 @@
62
62
  "./node_modules/@llblab/pi-actors/dist/pi-actors/index.js",
63
63
  "./node_modules/@llblab/pi-clean-room/index.ts",
64
64
  "./node_modules/@llblab/pi-codex-usage/index.ts",
65
- "./node_modules/@llblab/pi-grow-loop/index.ts",
65
+ "./node_modules/@llblab/pi-grow-loop/dist/pi-grow-loop/index.js",
66
66
  "./node_modules/@llblab/pi-state-flow/dist/pi-state-flow/index.js",
67
67
  "./node_modules/@llblab/pi-telegram/dist/pi-telegram/index.js"
68
68
  ],
69
69
  "skills": [
70
70
  "./node_modules/@llblab/pi-actors/dist/skills",
71
- "./node_modules/@llblab/pi-grow-loop/skills",
71
+ "./node_modules/@llblab/pi-grow-loop/dist/skills",
72
72
  "./node_modules/@llblab/pi-state-flow/dist/skills",
73
73
  "./node_modules/@llblab/pi-telegram/dist/skills",
74
74
  "./node_modules/@llblab/skills/"