@fml-inc/fml 0.8.0-rc.1 → 0.8.0-rc.3

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.
@@ -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
+ };
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runObserverSyncCycle
4
- } from "./chunk-CK5MEL5J.js";
5
- import "./chunk-JXSSPQ5B.js";
6
- import "./chunk-7DR5X7XI.js";
4
+ } from "./chunk-3KISRT57.js";
5
+ import "./chunk-YSU5CS2V.js";
6
+ import "./chunk-A6HAEVJX.js";
7
7
  import "./chunk-MLKGABMK.js";
8
8
 
9
9
  // src/sync-cycle.ts
@@ -10,7 +10,7 @@ import {
10
10
  setSelectedOrg,
11
11
  storeServiceRefreshToken,
12
12
  writeTokens
13
- } from "./chunk-7DR5X7XI.js";
13
+ } from "./chunk-A6HAEVJX.js";
14
14
  import "./chunk-MLKGABMK.js";
15
15
  export {
16
16
  SERVICE_TOKEN_LOGIN_USER_ID,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fml-inc/fml",
3
- "version": "0.8.0-rc.1",
3
+ "version": "0.8.0-rc.3",
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
- "@fml/panopticon": "0.0.0",
45
- "@fml/agent-targets": "0.0.0"
46
+ "@fml/agent-targets": "0.0.0",
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",