portable-agent-layer 0.6.2 → 0.7.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.6.2",
3
+ "version": "0.7.0",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli/index.ts CHANGED
@@ -122,6 +122,18 @@ async function session(sessionArgs: string[]) {
122
122
  // Silently ignore summary errors
123
123
  }
124
124
 
125
+ // Check for updates and display notice
126
+ try {
127
+ const { checkForUpdate, getUpdateNotice } = await import(
128
+ "../hooks/handlers/update-check"
129
+ );
130
+ await checkForUpdate();
131
+ const notice = getUpdateNotice();
132
+ if (notice) console.log(`\n${notice}`);
133
+ } catch {
134
+ // Non-critical
135
+ }
136
+
125
137
  process.exit(exitCode);
126
138
  }
127
139
 
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Update checker — detects if a newer version of PAL is available.
3
+ *
4
+ * Repo mode (.palroot exists): git fetch + compare HEAD vs origin/main
5
+ * Package mode: fetch npm registry for latest version vs installed
6
+ *
7
+ * Caches result in state/update-available.json. Checked at most once per hour.
8
+ */
9
+
10
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { resolve } from "node:path";
12
+ import { logDebug } from "../lib/log";
13
+ import { ensureDir, palPkg, paths } from "../lib/paths";
14
+
15
+ interface UpdateCache {
16
+ checkedAt: string;
17
+ available: boolean;
18
+ current: string;
19
+ latest: string;
20
+ mode: "repo" | "package";
21
+ }
22
+
23
+ const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
24
+
25
+ function cachePath(): string {
26
+ return resolve(ensureDir(paths.state()), "update-available.json");
27
+ }
28
+
29
+ function readCache(): UpdateCache | null {
30
+ try {
31
+ const fp = cachePath();
32
+ if (!existsSync(fp)) return null;
33
+ const cache = JSON.parse(readFileSync(fp, "utf-8")) as UpdateCache;
34
+ if (Date.now() - new Date(cache.checkedAt).getTime() < CACHE_TTL_MS) return cache;
35
+ return null; // expired
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function writeCache(cache: UpdateCache): void {
42
+ try {
43
+ writeFileSync(cachePath(), JSON.stringify(cache, null, 2), "utf-8");
44
+ } catch {
45
+ /* non-critical */
46
+ }
47
+ }
48
+
49
+ function isRepoMode(): boolean {
50
+ return existsSync(resolve(palPkg(), ".palroot"));
51
+ }
52
+
53
+ function getInstalledVersion(): string {
54
+ try {
55
+ const pkg = JSON.parse(readFileSync(resolve(palPkg(), "package.json"), "utf-8"));
56
+ return pkg.version || "0.0.0";
57
+ } catch {
58
+ return "0.0.0";
59
+ }
60
+ }
61
+
62
+ async function checkRepo(): Promise<UpdateCache> {
63
+ const repoDir = palPkg();
64
+ const current = getInstalledVersion();
65
+
66
+ try {
67
+ const fetch = Bun.spawn(["git", "fetch", "--quiet"], {
68
+ cwd: repoDir,
69
+ stdout: "ignore",
70
+ stderr: "ignore",
71
+ });
72
+ await fetch.exited;
73
+
74
+ const local = Bun.spawn(["git", "rev-parse", "HEAD"], {
75
+ cwd: repoDir,
76
+ stdout: "pipe",
77
+ stderr: "ignore",
78
+ });
79
+ const localHash = (await new Response(local.stdout).text()).trim();
80
+
81
+ const remote = Bun.spawn(["git", "rev-parse", "origin/main"], {
82
+ cwd: repoDir,
83
+ stdout: "pipe",
84
+ stderr: "ignore",
85
+ });
86
+ const remoteHash = (await new Response(remote.stdout).text()).trim();
87
+
88
+ const available = localHash !== remoteHash && remoteHash.length > 0;
89
+
90
+ // Get remote version from package.json on origin/main
91
+ let latest = current;
92
+ if (available) {
93
+ try {
94
+ const show = Bun.spawn(["git", "show", "origin/main:package.json"], {
95
+ cwd: repoDir,
96
+ stdout: "pipe",
97
+ stderr: "ignore",
98
+ });
99
+ const remotePkg = JSON.parse(await new Response(show.stdout).text());
100
+ latest = remotePkg.version || current;
101
+ } catch {
102
+ latest = `${remoteHash.slice(0, 7)} (ahead)`;
103
+ }
104
+ }
105
+
106
+ return {
107
+ checkedAt: new Date().toISOString(),
108
+ available,
109
+ current,
110
+ latest,
111
+ mode: "repo",
112
+ };
113
+ } catch {
114
+ return {
115
+ checkedAt: new Date().toISOString(),
116
+ available: false,
117
+ current,
118
+ latest: current,
119
+ mode: "repo",
120
+ };
121
+ }
122
+ }
123
+
124
+ async function checkNpm(): Promise<UpdateCache> {
125
+ const current = getInstalledVersion();
126
+
127
+ try {
128
+ const response = await fetch(
129
+ "https://registry.npmjs.org/portable-agent-layer/latest",
130
+ { signal: AbortSignal.timeout(5000) }
131
+ );
132
+
133
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
134
+ const data = (await response.json()) as { version?: string };
135
+ const latest = data.version || current;
136
+
137
+ return {
138
+ checkedAt: new Date().toISOString(),
139
+ available: latest !== current,
140
+ current,
141
+ latest,
142
+ mode: "package",
143
+ };
144
+ } catch {
145
+ return {
146
+ checkedAt: new Date().toISOString(),
147
+ available: false,
148
+ current,
149
+ latest: current,
150
+ mode: "package",
151
+ };
152
+ }
153
+ }
154
+
155
+ /** Run update check on Stop (non-blocking). Caches result for greeting. */
156
+ export async function checkForUpdate(): Promise<void> {
157
+ const cached = readCache();
158
+ if (cached) {
159
+ logDebug("update-check", "Using cached result");
160
+ return;
161
+ }
162
+
163
+ const result = isRepoMode() ? await checkRepo() : await checkNpm();
164
+ writeCache(result);
165
+
166
+ if (result.available) {
167
+ logDebug(
168
+ "update-check",
169
+ `Update available: ${result.current} → ${result.latest} (${result.mode})`
170
+ );
171
+ }
172
+ }
173
+
174
+ /** Read cached update status for greeting display. Returns null if no update. */
175
+ export function getUpdateNotice(): string | null {
176
+ try {
177
+ const fp = cachePath();
178
+ if (!existsSync(fp)) return null;
179
+ const cache = JSON.parse(readFileSync(fp, "utf-8")) as UpdateCache;
180
+ if (!cache.available) return null;
181
+
182
+ if (cache.mode === "repo") {
183
+ return `📦 Update available: ${cache.current} → ${cache.latest} (git pull)`;
184
+ }
185
+ return `📦 Update available: ${cache.current} → ${cache.latest} (bun update -g portable-agent-layer)`;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
@@ -30,6 +30,7 @@ export const HOOK_MANAGED_FILES = [
30
30
  "pending-failure.json",
31
31
  "token-usage.jsonl",
32
32
  "graduated.json",
33
+ "update-available.json",
33
34
  ];
34
35
 
35
36
  /** Hook-managed directories — AI must not write to or delete from these */