@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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +29 -16
- package/dist/{chunk-CK5MEL5J.js → chunk-3KISRT57.js} +1 -1
- package/dist/{chunk-HUR2HCUG.js → chunk-7DWFJENY.js} +2 -2
- package/dist/{chunk-T5YVLSUC.js → chunk-7V7G7NVF.js} +4 -0
- package/dist/{chunk-7DR5X7XI.js → chunk-A6HAEVJX.js} +136 -52
- package/dist/{chunk-F66IBM3Z.js → chunk-DZKYW327.js} +2 -2
- package/dist/{chunk-I4GZPTBM.js → chunk-ENW2GJNO.js} +1 -1
- package/dist/{chunk-DEMVOVVY.js → chunk-KBT5RMAU.js} +263 -32
- package/dist/{chunk-P3QLARXE.js → chunk-R45CWWZO.js} +3 -3
- package/dist/{chunk-JXSSPQ5B.js → chunk-YSU5CS2V.js} +1 -1
- package/dist/cli.js +1708 -1351
- package/dist/hook-handler.js +299 -42
- package/dist/mcp/server.js +7 -7
- package/dist/observer-server.js +8 -8
- package/dist/pi/extension.js +350 -0
- package/dist/sync-cycle.js +3 -3
- package/dist/{token-store-R7WXEPLI.js → token-store-BT5THIAJ.js} +1 -1
- package/package.json +6 -4
|
@@ -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/dist/sync-cycle.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
runObserverSyncCycle
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
6
|
-
import "./chunk-
|
|
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
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fml-inc/fml",
|
|
3
|
-
"version": "0.8.0-rc.
|
|
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/
|
|
45
|
-
"@fml/
|
|
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",
|