@fml-inc/fml 0.8.0-rc.2 → 0.8.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.
@@ -6,14 +6,13 @@ import {
6
6
  getActiveHookDestinationBinding
7
7
  } from "./chunk-DZKYW327.js";
8
8
  import {
9
- HOOK_TARGETS,
10
9
  SHELL_TOOL_NAMES,
11
10
  createHookInboxEntry,
12
11
  hookInboxDeliveryPayload,
13
12
  normalizeObserverHookCapture,
14
13
  persistHookInboxEntry,
15
14
  readObserverAuthToken
16
- } from "./chunk-56TPYMNK.js";
15
+ } from "./chunk-KBT5RMAU.js";
17
16
  import {
18
17
  resolveRepositoryRootFromCwd,
19
18
  resolveRepositorySlugFromCwd
@@ -262,8 +261,16 @@ var MAX_HOOK_STDIN_BYTES = 4 * 1024 * 1024;
262
261
  var HOOK_REQUEST_TIMEOUT_MS = 500;
263
262
  var HOOK_STDIN_TIMEOUT_MS = 5e3;
264
263
  var HOOK_REPO_RESOLUTION_TIMEOUT_MS = 250;
264
+ var INSTALLED_HOOK_TARGETS = Object.freeze([
265
+ "claude",
266
+ "codex",
267
+ "gemini",
268
+ "grok"
269
+ ]);
265
270
  function parseHookTarget(value) {
266
- return HOOK_TARGETS.includes(value) ? value : null;
271
+ return INSTALLED_HOOK_TARGETS.includes(
272
+ value
273
+ ) ? value : null;
267
274
  }
268
275
  function observerEndpoint(endpointFile) {
269
276
  try {
@@ -6,7 +6,7 @@ import {
6
6
  observerLeaseIsOwned,
7
7
  removeObserverStateIfOwned,
8
8
  updateObserverState
9
- } from "./chunk-WZLLO563.js";
9
+ } from "./chunk-7DWFJENY.js";
10
10
  import {
11
11
  createHookSyncLoop,
12
12
  getActiveHookDestinationBinding
@@ -25,7 +25,7 @@ import {
25
25
  parseHookInboxMetadata,
26
26
  readHookInboxStatusAsync,
27
27
  retireHookInboxEntry
28
- } from "./chunk-56TPYMNK.js";
28
+ } from "./chunk-KBT5RMAU.js";
29
29
  import "./chunk-7V7G7NVF.js";
30
30
  import {
31
31
  ObserverSyncCycleAbortedError,
@@ -1239,7 +1239,7 @@ var hookRejectionStatus = () => ({
1239
1239
  ...lastRejectedReason !== void 0 ? { lastRejectedReason } : {},
1240
1240
  ...lastRejectedTarget !== void 0 ? { lastRejectedTarget } : {}
1241
1241
  });
1242
- var packageVersion = true ? "0.8.0-rc.2+bed6eaed" : "dev";
1242
+ var packageVersion = true ? "0.8.0+246af775" : "dev";
1243
1243
  var health = () => ({
1244
1244
  status: "ok",
1245
1245
  service: OBSERVER_SERVICE_NAME,
@@ -0,0 +1,350 @@
1
+ // src/pi/extension.ts
2
+ import { randomUUID as randomUUID2 } from "node:crypto";
3
+ import fs2 from "node:fs";
4
+ import os from "node:os";
5
+ import path2 from "node:path";
6
+
7
+ // ../../packages/agent-targets/src/install/pi/index.ts
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { randomUUID } from "node:crypto";
11
+ var FML_PI_EXTENSION_MARKER = "__FML_PI_EXTENSION_V1__";
12
+ var LEGACY_PI_EXTENSION_MARKER = "__panopticonPiExtensionTest";
13
+ var LEGACY_PI_EXTENSION_SIGNATURES = Object.freeze([
14
+ "PANOPTICON_PORT",
15
+ '"/hooks"',
16
+ "UserPromptSubmit",
17
+ "PreToolUse",
18
+ "PostToolUse",
19
+ "SessionEnd"
20
+ ]);
21
+ function pathApiFor(platform) {
22
+ return platform === "win32" ? path.win32 : path.posix;
23
+ }
24
+ function resolvePiAgentDir(context, pathApi) {
25
+ const configured = context.env.PI_CODING_AGENT_DIR?.trim();
26
+ if (!configured) {
27
+ return pathApi.join(context.homeDir, ".pi", "agent");
28
+ }
29
+ if (configured === "~") return context.homeDir;
30
+ if (configured.startsWith("~/") || configured.startsWith("~\\")) {
31
+ return pathApi.join(context.homeDir, configured.slice(2));
32
+ }
33
+ return pathApi.normalize(configured);
34
+ }
35
+ function readFileIfPresent(filePath) {
36
+ try {
37
+ return fs.readFileSync(filePath, "utf8");
38
+ } catch (error) {
39
+ if (error.code === "ENOENT") return null;
40
+ throw error;
41
+ }
42
+ }
43
+ function isOwnedExtension(source, legacy = false) {
44
+ return source.includes(FML_PI_EXTENSION_MARKER) || legacy && (source.includes(LEGACY_PI_EXTENSION_MARKER) || LEGACY_PI_EXTENSION_SIGNATURES.every(
45
+ (signature) => source.includes(signature)
46
+ ));
47
+ }
48
+ function removeLegacyExtension(filePath) {
49
+ const source = readFileIfPresent(filePath);
50
+ if (source === null) return { removed: false, preserved: false };
51
+ if (!isOwnedExtension(source, true)) {
52
+ return { removed: false, preserved: true };
53
+ }
54
+ fs.rmSync(filePath);
55
+ return { removed: true, preserved: false };
56
+ }
57
+ function writeExtensionAtomically(filePath, source) {
58
+ fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 448 });
59
+ const temporaryPath = path.join(
60
+ path.dirname(filePath),
61
+ `.${path.basename(filePath)}.${process.pid}.${randomUUID()}.tmp`
62
+ );
63
+ try {
64
+ fs.writeFileSync(temporaryPath, source, { encoding: "utf8", mode: 384 });
65
+ if (process.platform === "win32" && fs.existsSync(filePath)) {
66
+ fs.rmSync(filePath);
67
+ }
68
+ fs.renameSync(temporaryPath, filePath);
69
+ } finally {
70
+ fs.rmSync(temporaryPath, { force: true });
71
+ }
72
+ }
73
+ var PI_EXTENSION_INSTALLER = Object.freeze({
74
+ resolvePaths(context) {
75
+ const pathApi = pathApiFor(context.platform);
76
+ const extensionsDir = pathApi.join(
77
+ resolvePiAgentDir(context, pathApi),
78
+ "extensions"
79
+ );
80
+ return {
81
+ sourcePath: pathApi.join(
82
+ context.packageRoot,
83
+ "dist",
84
+ "pi",
85
+ "extension.js"
86
+ ),
87
+ extensionsDir,
88
+ destinationPath: pathApi.join(extensionsDir, "fml.js"),
89
+ legacyDestinationPath: pathApi.join(extensionsDir, "panopticon.js")
90
+ };
91
+ },
92
+ install(paths) {
93
+ const source = fs.readFileSync(paths.sourcePath, "utf8");
94
+ if (!source.includes(FML_PI_EXTENSION_MARKER)) {
95
+ throw new Error(
96
+ `Packaged Pi extension is invalid: ${paths.sourcePath} does not contain the FML ownership marker`
97
+ );
98
+ }
99
+ const existing = readFileIfPresent(paths.destinationPath);
100
+ if (existing !== null && !isOwnedExtension(existing)) {
101
+ const legacy2 = removeLegacyExtension(paths.legacyDestinationPath);
102
+ return {
103
+ status: "preserved",
104
+ destinationPath: paths.destinationPath,
105
+ reason: "an extension not managed by FML already exists at this path",
106
+ legacyRemoved: legacy2.removed,
107
+ legacyPreserved: legacy2.preserved
108
+ };
109
+ }
110
+ const status = existing === null ? "installed" : existing === source ? "unchanged" : "updated";
111
+ if (status !== "unchanged") {
112
+ writeExtensionAtomically(paths.destinationPath, source);
113
+ }
114
+ const legacy = removeLegacyExtension(paths.legacyDestinationPath);
115
+ return {
116
+ status,
117
+ destinationPath: paths.destinationPath,
118
+ legacyRemoved: legacy.removed,
119
+ legacyPreserved: legacy.preserved
120
+ };
121
+ },
122
+ uninstall(paths) {
123
+ const existing = readFileIfPresent(paths.destinationPath);
124
+ const legacy = removeLegacyExtension(paths.legacyDestinationPath);
125
+ if (existing === null) {
126
+ return {
127
+ status: "absent",
128
+ destinationPath: paths.destinationPath,
129
+ legacyRemoved: legacy.removed,
130
+ legacyPreserved: legacy.preserved
131
+ };
132
+ }
133
+ if (!isOwnedExtension(existing)) {
134
+ return {
135
+ status: "preserved",
136
+ destinationPath: paths.destinationPath,
137
+ reason: "the existing extension is not managed by FML",
138
+ legacyRemoved: legacy.removed,
139
+ legacyPreserved: legacy.preserved
140
+ };
141
+ }
142
+ fs.rmSync(paths.destinationPath);
143
+ return {
144
+ status: "removed",
145
+ destinationPath: paths.destinationPath,
146
+ legacyRemoved: legacy.removed,
147
+ legacyPreserved: legacy.preserved
148
+ };
149
+ }
150
+ });
151
+
152
+ // src/pi/extension.ts
153
+ var DEFAULT_REQUEST_TIMEOUT_MS = 500;
154
+ var DEFAULT_SHUTDOWN_FLUSH_TIMEOUT_MS = 5e3;
155
+ var pendingPosts = /* @__PURE__ */ new Set();
156
+ function positiveInteger(value, fallback) {
157
+ if (!value) return fallback;
158
+ const parsed = Number.parseInt(value, 10);
159
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
160
+ }
161
+ function absoluteEnvPath(value) {
162
+ if (!value?.trim()) return void 0;
163
+ const trimmed = value.trim();
164
+ if (trimmed === "~") return os.homedir();
165
+ if (trimmed.startsWith("~/") || trimmed.startsWith("~\\")) {
166
+ return path2.join(os.homedir(), trimmed.slice(2));
167
+ }
168
+ return path2.isAbsolute(trimmed) ? path2.normalize(trimmed) : void 0;
169
+ }
170
+ function observerDirectory() {
171
+ const fmlHome = absoluteEnvPath(process.env.FML_HOME);
172
+ return absoluteEnvPath(process.env.FML_OBSERVER_DIR) ?? absoluteEnvPath(process.env.PANOPTICON_HOME) ?? (fmlHome ? path2.join(fmlHome, "panopticon") : path2.join(os.homedir(), ".fml", "panopticon"));
173
+ }
174
+ function readObserverEndpoint(filePath) {
175
+ try {
176
+ const parsed = JSON.parse(fs2.readFileSync(filePath, "utf8"));
177
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
178
+ return null;
179
+ }
180
+ const record = parsed;
181
+ if (record.host !== "127.0.0.1" && record.host !== "::1" && record.host !== "localhost" || typeof record.port !== "number" || !Number.isInteger(record.port) || record.port <= 0 || record.port > 65535) {
182
+ return null;
183
+ }
184
+ return { host: record.host, port: record.port };
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ function readObserverAuthToken(filePath) {
190
+ try {
191
+ return fs2.readFileSync(filePath, "utf8").trim() || null;
192
+ } catch {
193
+ return null;
194
+ }
195
+ }
196
+ async function deliver(event) {
197
+ const directory = observerDirectory();
198
+ const endpoint = readObserverEndpoint(path2.join(directory, "endpoint.json"));
199
+ const authToken = readObserverAuthToken(path2.join(directory, "auth-token"));
200
+ if (!endpoint || !authToken) return;
201
+ const host = endpoint.host === "::1" ? "[::1]" : endpoint.host;
202
+ try {
203
+ const response = await fetch(`http://${host}:${endpoint.port}/hooks`, {
204
+ method: "POST",
205
+ headers: {
206
+ Authorization: `Bearer ${authToken}`,
207
+ "Content-Type": "application/json"
208
+ },
209
+ body: JSON.stringify({
210
+ target: "pi",
211
+ payload: event,
212
+ ...event.cwd ? { cwd: event.cwd } : {}
213
+ }),
214
+ signal: AbortSignal.timeout(
215
+ positiveInteger(
216
+ process.env.FML_PI_REQUEST_TIMEOUT_MS,
217
+ DEFAULT_REQUEST_TIMEOUT_MS
218
+ )
219
+ )
220
+ });
221
+ await response.arrayBuffer();
222
+ } catch {
223
+ }
224
+ }
225
+ function post(event) {
226
+ const request = deliver(event);
227
+ pendingPosts.add(request);
228
+ void request.finally(() => pendingPosts.delete(request));
229
+ }
230
+ async function flushPendingPosts(timeoutMs = positiveInteger(
231
+ process.env.FML_PI_SHUTDOWN_FLUSH_TIMEOUT_MS,
232
+ DEFAULT_SHUTDOWN_FLUSH_TIMEOUT_MS
233
+ )) {
234
+ if (pendingPosts.size === 0) return;
235
+ await Promise.race([
236
+ Promise.allSettled([...pendingPosts]),
237
+ new Promise((resolve) => setTimeout(resolve, timeoutMs))
238
+ ]);
239
+ }
240
+ function modelLabel(model) {
241
+ if (!model || typeof model !== "object") return void 0;
242
+ const record = model;
243
+ const id = record.id ?? record.name ?? record.model;
244
+ const provider = record.provider ?? record.providerId;
245
+ return [provider, id].filter((value) => typeof value === "string" && value.length > 0).join(":") || void 0;
246
+ }
247
+ function fmlPiExtension(pi) {
248
+ let sessionId = randomUUID2();
249
+ let cwd;
250
+ function emit(event) {
251
+ post({
252
+ ...event,
253
+ session_id: sessionId,
254
+ source: "pi",
255
+ cwd
256
+ });
257
+ }
258
+ pi.on("session_start", async (_event, context) => {
259
+ sessionId = context.sessionManager.getSessionId();
260
+ cwd = context.cwd;
261
+ emit({ hook_event_name: "SessionStart" });
262
+ });
263
+ pi.on("input", async (event) => {
264
+ emit({ hook_event_name: "UserPromptSubmit", prompt: event.text });
265
+ });
266
+ pi.on("turn_start", async (event) => {
267
+ emit({
268
+ hook_event_name: "TurnStart",
269
+ turn_index: event.turnIndex,
270
+ pi_timestamp: event.timestamp
271
+ });
272
+ });
273
+ pi.on("turn_end", async (event) => {
274
+ emit({
275
+ hook_event_name: "Stop",
276
+ turn_index: event.turnIndex,
277
+ assistant_message: event.message
278
+ });
279
+ });
280
+ pi.on("tool_call", async (event) => {
281
+ emit({
282
+ hook_event_name: "PreToolUse",
283
+ tool_name: event.toolName,
284
+ tool_call_id: event.toolCallId,
285
+ tool_input: event.input
286
+ });
287
+ });
288
+ pi.on("tool_result", async (event) => {
289
+ emit({
290
+ hook_event_name: event.isError ? "PostToolUseFailure" : "PostToolUse",
291
+ tool_name: event.toolName,
292
+ tool_call_id: event.toolCallId,
293
+ tool_input: event.input,
294
+ tool_result: { content: event.content, details: event.details }
295
+ });
296
+ });
297
+ pi.on("session_before_compact", async (event) => {
298
+ emit({
299
+ hook_event_name: "PreCompact",
300
+ branch_entry_count: event.branchEntries.length,
301
+ custom_instructions: event.customInstructions
302
+ });
303
+ });
304
+ pi.on("session_compact", async (event) => {
305
+ emit({
306
+ hook_event_name: "PostCompact",
307
+ compaction_entry: event.compactionEntry,
308
+ from_extension: event.fromExtension
309
+ });
310
+ });
311
+ pi.on("model_select", async (event) => {
312
+ emit({
313
+ hook_event_name: "ConfigChange",
314
+ config_key: "model",
315
+ config_source: event.source,
316
+ previous_value: modelLabel(event.previousModel),
317
+ new_value: modelLabel(event.model)
318
+ });
319
+ });
320
+ pi.on("thinking_level_select", async (event) => {
321
+ emit({
322
+ hook_event_name: "ConfigChange",
323
+ config_key: "thinking_level",
324
+ previous_value: event.previousLevel,
325
+ new_value: event.level
326
+ });
327
+ });
328
+ pi.on("user_bash", async (event) => {
329
+ emit({
330
+ hook_event_name: "Notification",
331
+ notification_type: "user_bash",
332
+ command: event.command,
333
+ exclude_from_context: event.excludeFromContext,
334
+ event_cwd: event.cwd
335
+ });
336
+ });
337
+ pi.on("session_shutdown", async () => {
338
+ emit({ hook_event_name: "SessionEnd" });
339
+ await flushPendingPosts();
340
+ });
341
+ }
342
+ var __fmlPiExtensionTest = {
343
+ flushPendingPosts,
344
+ pendingPostCount: () => pendingPosts.size
345
+ };
346
+ export {
347
+ FML_PI_EXTENSION_MARKER,
348
+ __fmlPiExtensionTest,
349
+ fmlPiExtension as default
350
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fml-inc/fml",
3
- "version": "0.8.0-rc.2",
3
+ "version": "0.8.0",
4
4
  "description": "FML CLI and agent tools",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -37,15 +37,17 @@
37
37
  "zod": "4.1.12"
38
38
  },
39
39
  "devDependencies": {
40
+ "@earendil-works/pi-coding-agent": "^0.83.0",
40
41
  "@sentry/core": "^10.43.0",
41
42
  "@types/node": "^22.0.0",
43
+ "esbuild": "^0.27.1",
42
44
  "tsup": "^8.0.0",
43
45
  "typescript": "^5",
44
46
  "@fml/agent-targets": "0.0.0",
45
47
  "@fml/panopticon": "0.0.0"
46
48
  },
47
49
  "scripts": {
48
- "build": "node scripts/generate-plugin-manifest.js && tsup",
50
+ "build": "node scripts/generate-plugin-manifest.js && tsup && node scripts/build-pi-extension.mjs",
49
51
  "dev": "tsup --watch",
50
52
  "pack:local": "pnpm pack",
51
53
  "type-check": "tsc --noEmit",