@llblab/pi-telegram 0.51.3 → 0.51.4

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/AGENTS.md CHANGED
@@ -152,7 +152,7 @@ Before non-trivial work:
152
152
  While working:
153
153
 
154
154
  - Keep changes inside this repository; updating an installed Pi checkout is a separate operator action.
155
- - Rebuild the package with `npm run build` after edits. Pi loads `dist/pi-telegram/index.js`, so source-only changes are not live and `/reload` or process restart alone will reload stale compiled output.
155
+ - Rebuild the package with `npm run build` after edits. Pi loads `dist/pi-telegram/index.js`, so source-only changes are not live and `/reload` or process restart alone will reload stale compiled output. Keep the committed `dist/` synchronized for Git installs; builds use a temporary candidate and rollback-safe swap, while `npm run build:check` rejects drift without rewriting the tree.
156
156
  - Read large artifacts search-first and range-bounded. For `CHANGELOG.md`, inspect only the current release section unless older history is relevant.
157
157
  - Keep successful validation output compact; inspect focused failure tails. Prefer focused tests/typecheck during iteration and broad validation at a stable gate.
158
158
  - Preserve unrelated work and do not commit, publish, tag, deploy, or perform external actions without explicit authorization.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,11 @@
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 0.51.4: Resolver-owned Skills and drift-safe Git installs
8
+
9
+ - `Resolver-owned Skills`: Auto-discovered user/project checkouts now contribute source Skills even when Pi selects their compiled entrypoint, while manifest-loaded npm, Git, and Pi Kit packages retain `pi.skills` filters and package provenance. Resolver regressions cover checkout and filtered-package behavior; unsupported source manifest aliases are removed.
10
+ - `Drift-safe distributive`: The compiled runtime and Skill tree are now committed for self-contained Git installs. Builds compile into a temporary candidate and use a rollback-safe swap; validation rejects stale committed output without silently repairing it.
11
+
7
12
  ## 0.51.3: Follower recovery and filterable Skills
8
13
 
9
14
  - `Follower slot reconciliation`: Re-registration now treats the exact Workspace claim as canonical when a retained follower record carries a different slot. It repairs the record before binding commit, preserves the unrelated binding that owns the stale letter, and makes repeated registration idempotent instead of returning `Telegram Workspace binding claim changed.` forever.
package/README.md CHANGED
@@ -26,7 +26,7 @@ From git:
26
26
  pi install git:github.com/llblab/pi-telegram
27
27
  ```
28
28
 
29
- Installed npm/git packages expose bundled Skills through their `pi.skills` manifest, so Pi package filters can select individual Skills. A raw TypeScript checkout placed directly under Pi's `extensions` directory instead contributes its adjacent source Skills at runtime; the two discovery paths are mutually exclusive.
29
+ Installed npm/git packages expose bundled Skills through their `pi.skills` manifest, so Pi package filters and package provenance remain authoritative. A checkout auto-discovered directly under Pi's user or project `extensions` directory contributes its adjacent source Skills even when Pi selects the checkout's compiled entrypoint; the two discovery paths are mutually exclusive.
30
30
 
31
31
  The extension requires Pi `0.84.4` or newer, matching the package's peer dependencies. Its Activity API uses the public `agent_settled` lifecycle event to keep retries/continuations under one activity identity and release that identity only after the run fully settles.
32
32
 
@@ -319,7 +319,7 @@ The docs index lives at [docs/README.md](./docs/README.md).
319
319
 
320
320
  ## Development
321
321
 
322
- Pi loads the compiled `dist/pi-telegram/index.js` entrypoint. After every project change, run `npm run build` before `/reload`, restart, or live verification; reloading source without rebuilding can leave the running extension on stale compiled code.
322
+ Pi loads the compiled `dist/pi-telegram/index.js` entrypoint. The committed distributive also makes Git installs self-contained. After every project change, run `npm run build` before `/reload`, restart, or live verification; `npm run build:check` verifies source/artifact synchronization without rewriting it.
323
323
 
324
324
  ```bash
325
325
  npm run build
@@ -4,5 +4,11 @@
4
4
  * Owns source-checkout skill contribution; installed packages use their manifest
5
5
  */
6
6
  import type { ExtensionAPI } from "./pi.ts";
7
+ export declare function getTelegramExtensionPackageRoot(modulePath: string): string;
8
+ export interface TelegramRawExtensionCheckoutOptions {
9
+ agentDir?: string;
10
+ cwd?: string;
11
+ }
12
+ export declare function isRawTelegramExtensionCheckout(modulePath: string, options?: TelegramRawExtensionCheckoutOptions): boolean;
7
13
  export declare const TELEGRAM_SKILLS_PATH: string;
8
- export declare function registerTelegramSkillDiscovery(pi: Pick<ExtensionAPI, "on">, modulePath?: string): boolean;
14
+ export declare function registerTelegramSkillDiscovery(pi: Pick<ExtensionAPI, "on">, modulePath?: string, options?: TelegramRawExtensionCheckoutOptions): boolean;
@@ -3,17 +3,42 @@
3
3
  * Zones: pi agent, telegram guidance
4
4
  * Owns source-checkout skill contribution; installed packages use their manifest
5
5
  */
6
- import { extname } from "node:path";
6
+ import { existsSync } from "node:fs";
7
+ import { basename, dirname, join, resolve } from "node:path";
7
8
  import { fileURLToPath } from "node:url";
9
+ import { resolveAgentDir } from "./paths.js";
8
10
  const TELEGRAM_SKILLS_MODULE_PATH = fileURLToPath(import.meta.url);
9
- export const TELEGRAM_SKILLS_PATH = fileURLToPath(new URL("../skills", import.meta.url));
10
- export function registerTelegramSkillDiscovery(pi, modulePath = TELEGRAM_SKILLS_MODULE_PATH) {
11
- // A raw source extension has no package manifest owner. Compiled npm/git
12
- // packages do, so contributing again would bypass their resource filters.
13
- if (extname(modulePath) !== ".ts")
11
+ export function getTelegramExtensionPackageRoot(modulePath) {
12
+ let current = dirname(modulePath);
13
+ while (true) {
14
+ if (existsSync(join(current, "package.json"))) {
15
+ const parent = dirname(current);
16
+ if (basename(current) === "dist" && existsSync(join(parent, "package.json"))) {
17
+ return parent;
18
+ }
19
+ return current;
20
+ }
21
+ const parent = dirname(current);
22
+ if (parent === current)
23
+ return dirname(modulePath);
24
+ current = parent;
25
+ }
26
+ }
27
+ export function isRawTelegramExtensionCheckout(modulePath, options = {}) {
28
+ const packageRoot = resolve(getTelegramExtensionPackageRoot(modulePath));
29
+ const agentDir = resolve(options.agentDir ?? resolveAgentDir());
30
+ const cwd = resolve(options.cwd ?? process.cwd());
31
+ return dirname(packageRoot) === join(agentDir, "extensions") ||
32
+ dirname(packageRoot) === join(cwd, ".pi", "extensions");
33
+ }
34
+ export const TELEGRAM_SKILLS_PATH = join(getTelegramExtensionPackageRoot(TELEGRAM_SKILLS_MODULE_PATH), "skills");
35
+ export function registerTelegramSkillDiscovery(pi, modulePath = TELEGRAM_SKILLS_MODULE_PATH, options = {}) {
36
+ // Pi auto-discovers extension entrypoints but not their manifest Skills.
37
+ // Manifest-loaded packages own Skills and filters through `pi.skills`.
38
+ if (!isRawTelegramExtensionCheckout(modulePath, options))
14
39
  return false;
15
40
  pi.on("resources_discover", () => ({
16
- skillPaths: [TELEGRAM_SKILLS_PATH],
41
+ skillPaths: [join(getTelegramExtensionPackageRoot(modulePath), "skills")],
17
42
  }));
18
43
  return true;
19
44
  }
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.3",
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": {
package/lib/skills.ts CHANGED
@@ -4,26 +4,62 @@
4
4
  * Owns source-checkout skill contribution; installed packages use their manifest
5
5
  */
6
6
 
7
- import { extname } from "node:path";
7
+ import { existsSync } from "node:fs";
8
+ import { basename, dirname, join, resolve } from "node:path";
8
9
  import { fileURLToPath } from "node:url";
9
10
 
10
11
  import type { ExtensionAPI } from "./pi.ts";
12
+ import { resolveAgentDir } from "./paths.ts";
11
13
 
12
14
  const TELEGRAM_SKILLS_MODULE_PATH = fileURLToPath(import.meta.url);
13
15
 
14
- export const TELEGRAM_SKILLS_PATH = fileURLToPath(
15
- new URL("../skills", import.meta.url),
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",
16
51
  );
17
52
 
18
53
  export function registerTelegramSkillDiscovery(
19
54
  pi: Pick<ExtensionAPI, "on">,
20
55
  modulePath = TELEGRAM_SKILLS_MODULE_PATH,
56
+ options: TelegramRawExtensionCheckoutOptions = {},
21
57
  ): boolean {
22
- // A raw source extension has no package manifest owner. Compiled npm/git
23
- // packages do, so contributing again would bypass their resource filters.
24
- if (extname(modulePath) !== ".ts") return false;
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;
25
61
  pi.on("resources_discover", () => ({
26
- skillPaths: [TELEGRAM_SKILLS_PATH],
62
+ skillPaths: [join(getTelegramExtensionPackageRoot(modulePath), "skills")],
27
63
  }));
28
64
  return true;
29
65
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@llblab/pi-telegram",
3
- "version": "0.51.3",
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
+ }