@agimon-ai/po-file-edit 0.0.0-alpha.2 → 0.0.0-alpha.21
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/LICENSE +149 -21
- package/README.md +3 -3
- package/dist/apiContracts.cjs +1 -1
- package/dist/apiContracts.cjs.map +1 -1
- package/dist/apiContracts.mjs +1 -1
- package/dist/apiContracts.mjs.map +1 -1
- package/dist/extensions/native.mjs +10 -963
- package/dist/extensions/native.mjs.map +1 -1
- package/dist/extensions/server.cjs +337 -111
- package/dist/extensions/server.cjs.map +1 -1
- package/dist/extensions/server.d.mts +2 -2
- package/dist/extensions/server.d.mts.map +1 -1
- package/dist/extensions/server.mjs +363 -138
- package/dist/extensions/server.mjs.map +1 -1
- package/dist/extensions/web.mjs +44 -354
- package/dist/extensions/web.mjs.map +1 -1
- package/package.json +24 -23
|
@@ -24,56 +24,211 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
enumerable: true
|
|
25
25
|
}) : target, mod));
|
|
26
26
|
//#endregion
|
|
27
|
-
let
|
|
27
|
+
let _agimon_ai_po_core_serverPlugin = require("@agimon-ai/po-core/serverPlugin");
|
|
28
28
|
let _agimon_ai_po_core_extensionFile = require("@agimon-ai/po-core/extensionFile");
|
|
29
|
-
let
|
|
30
|
-
|
|
29
|
+
let _agimon_ai_po_core_childSession = require("@agimon-ai/po-core/childSession");
|
|
30
|
+
let _agimon_ai_po_core_hubChannel = require("@agimon-ai/po-core/hubChannel");
|
|
31
|
+
let node_fs_promises = require("node:fs/promises");
|
|
32
|
+
node_fs_promises = __toESM(node_fs_promises, 1);
|
|
31
33
|
let node_path = require("node:path");
|
|
32
34
|
node_path = __toESM(node_path, 1);
|
|
35
|
+
let _agimon_ai_po_hashline = require("@agimon-ai/po-hashline");
|
|
36
|
+
let _agimon_ai_po_hashline_files = require("@agimon-ai/po-hashline/files");
|
|
37
|
+
let node_fs = require("node:fs");
|
|
38
|
+
node_fs = __toESM(node_fs, 1);
|
|
33
39
|
let _agimon_ai_po_core_childProcess = require("@agimon-ai/po-core/childProcess");
|
|
34
40
|
let _agimon_ai_po_telemetry = require("@agimon-ai/po-telemetry");
|
|
35
|
-
let ignore = require("ignore");
|
|
36
|
-
ignore = __toESM(ignore, 1);
|
|
37
|
-
let node_child_process = require("node:child_process");
|
|
38
41
|
let node_crypto = require("node:crypto");
|
|
39
42
|
let _agimon_ai_po_core_piSettings = require("@agimon-ai/po-core/piSettings");
|
|
43
|
+
let node_child_process = require("node:child_process");
|
|
40
44
|
let node_util = require("node:util");
|
|
41
|
-
let
|
|
42
|
-
|
|
45
|
+
let ignore = require("ignore");
|
|
46
|
+
ignore = __toESM(ignore, 1);
|
|
47
|
+
let node_os = require("node:os");
|
|
48
|
+
node_os = __toESM(node_os, 1);
|
|
49
|
+
let _agimon_ai_po_config = require("@agimon-ai/po-config");
|
|
43
50
|
let hono = require("hono");
|
|
44
|
-
//#region src/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
//#region src/constants/editPresence.ts
|
|
52
|
+
const PRESENCE_NOTE_TITLE = "[Po: other active agents edited files in this result]";
|
|
53
|
+
const PRESENCE_NOTE_GUIDANCE = "Preserve unrelated edits. If coordination would help, you can use intercom to send to the session id, or ask the subagent by name if it is on your team. Contact is optional; this notice does not block editing.";
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/services/editPresence/index.ts
|
|
56
|
+
const PATH_TOOLS = /* @__PURE__ */ new Set([
|
|
57
|
+
"read",
|
|
58
|
+
"edit",
|
|
59
|
+
"write"
|
|
60
|
+
]);
|
|
61
|
+
const EDIT_TOOLS = /* @__PURE__ */ new Set(["edit", "write"]);
|
|
62
|
+
const MAX_GREP_FILES = 50;
|
|
63
|
+
const MAX_NOTE_LINES = 10;
|
|
64
|
+
const MINUTE_MS = 6e4;
|
|
65
|
+
const SHORT_ID_LENGTH = 8;
|
|
66
|
+
/** Pi's built-in grep prints match rows as `relative/path:line: text`. */
|
|
67
|
+
const PI_GREP_ROW = /^(.+?):(\d+): /u;
|
|
68
|
+
function forgetPresence(ledger, agentKey) {
|
|
69
|
+
for (const [file, entries] of ledger) {
|
|
70
|
+
entries.delete(agentKey);
|
|
71
|
+
if (entries.size === 0) ledger.delete(file);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function stringArg(args, key) {
|
|
75
|
+
if (typeof args !== "object" || args === null) return void 0;
|
|
76
|
+
const value = args[key];
|
|
77
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
78
|
+
}
|
|
79
|
+
function textOf(content) {
|
|
80
|
+
return content.map((part) => part.type === "text" && "text" in part && typeof part.text === "string" ? part.text : "").join("\n");
|
|
81
|
+
}
|
|
82
|
+
async function grepFiles(call) {
|
|
83
|
+
const lines = textOf(call.content).split("\n");
|
|
84
|
+
const headers = lines.flatMap((line) => {
|
|
85
|
+
const header = (0, _agimon_ai_po_hashline.parseFileHeader)(line);
|
|
86
|
+
return header === void 0 ? [] : [(0, _agimon_ai_po_hashline_files.resolveInputPath)(header.path, call.cwd)];
|
|
87
|
+
});
|
|
88
|
+
if (headers.length > 0) return [...new Set(headers)].slice(0, MAX_GREP_FILES);
|
|
89
|
+
const rows = lines.flatMap((line) => {
|
|
90
|
+
const match = PI_GREP_ROW.exec(line)?.[1];
|
|
91
|
+
return match === void 0 ? [] : [match];
|
|
92
|
+
});
|
|
93
|
+
if (rows.length === 0) return [];
|
|
94
|
+
const searchPath = (0, _agimon_ai_po_hashline_files.resolveInputPath)(stringArg(call.args, "path") ?? ".", call.cwd);
|
|
95
|
+
if ((await node_fs_promises.default.stat(searchPath).catch(() => void 0))?.isFile()) return [searchPath];
|
|
96
|
+
return [...new Set(rows.map((row) => node_path.default.resolve(searchPath, row)))].slice(0, MAX_GREP_FILES);
|
|
97
|
+
}
|
|
98
|
+
async function touchedFiles(call) {
|
|
99
|
+
if (PATH_TOOLS.has(call.toolName)) {
|
|
100
|
+
const input = stringArg(call.args, "path");
|
|
101
|
+
return input === void 0 ? [] : [await (0, _agimon_ai_po_hashline_files.resolveReadInputPath)(input, call.cwd)];
|
|
102
|
+
}
|
|
103
|
+
return call.toolName === "grep" ? grepFiles(call) : [];
|
|
104
|
+
}
|
|
105
|
+
function ago(elapsed) {
|
|
106
|
+
const minutes = Math.floor(elapsed / MINUTE_MS);
|
|
107
|
+
return minutes < 1 ? "under 1m ago" : `${minutes}m ago`;
|
|
108
|
+
}
|
|
109
|
+
function liveOthers(ledger, file, self) {
|
|
110
|
+
const entries = ledger.get(file);
|
|
111
|
+
if (entries === void 0) return [];
|
|
112
|
+
const live = [];
|
|
113
|
+
for (const [key, entry] of entries) {
|
|
114
|
+
if (key === self) continue;
|
|
115
|
+
if (entry.agent.isLive()) live.push(entry);
|
|
116
|
+
else entries.delete(key);
|
|
117
|
+
}
|
|
118
|
+
return live.sort((left, right) => right.at - left.at);
|
|
119
|
+
}
|
|
120
|
+
function record(ledger, file, agent, tag, at) {
|
|
121
|
+
const entries = ledger.get(file) ?? /* @__PURE__ */ new Map();
|
|
122
|
+
entries.set(agent.key, {
|
|
123
|
+
agent,
|
|
124
|
+
tag,
|
|
125
|
+
at
|
|
126
|
+
});
|
|
127
|
+
ledger.set(file, entries);
|
|
128
|
+
}
|
|
129
|
+
function renderNote(lines) {
|
|
130
|
+
const shown = lines.slice(0, MAX_NOTE_LINES);
|
|
131
|
+
const more = lines.length - shown.length;
|
|
132
|
+
return [
|
|
133
|
+
PRESENCE_NOTE_TITLE,
|
|
134
|
+
...shown,
|
|
135
|
+
...more > 0 ? [`- and ${more} more`] : [],
|
|
136
|
+
PRESENCE_NOTE_GUIDANCE
|
|
137
|
+
].join("\n");
|
|
49
138
|
}
|
|
50
139
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* record, and once when deciding what to list. A caller that built its own
|
|
55
|
-
* matcher would let the two drift apart.
|
|
56
|
-
*
|
|
57
|
-
* The test answers false for anything it cannot safely match, so a path that
|
|
58
|
-
* escapes the project stays visible rather than being hidden by accident.
|
|
140
|
+
* Notes live editors of the files a tool call touched, then records the caller's own successful edit.
|
|
141
|
+
* Returns the content with one note appended, or undefined when there is nothing to add, so callers
|
|
142
|
+
* never rewrite a result needlessly.
|
|
59
143
|
*/
|
|
60
|
-
function
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
144
|
+
async function observePresence(ledger, agent, call, now = Date.now()) {
|
|
145
|
+
const recording = EDIT_TOOLS.has(call.toolName) && !call.isError;
|
|
146
|
+
if (!recording && ledger.size === 0) return void 0;
|
|
147
|
+
const lines = [];
|
|
148
|
+
for (const file of await touchedFiles(call)) {
|
|
149
|
+
const real = await node_fs_promises.default.realpath(file).catch(() => void 0);
|
|
150
|
+
if (real === void 0) continue;
|
|
151
|
+
const others = liveOthers(ledger, real, agent.key);
|
|
152
|
+
let bytes;
|
|
153
|
+
if (others.length > 0) {
|
|
154
|
+
bytes = await node_fs_promises.default.readFile(real).catch(() => void 0);
|
|
155
|
+
const current = bytes === void 0 ? void 0 : (0, _agimon_ai_po_hashline_files.computeFileTag)(bytes);
|
|
156
|
+
const shown = (0, _agimon_ai_po_hashline_files.displayPath)(file, call.cwd);
|
|
157
|
+
for (const entry of others) {
|
|
158
|
+
const state = entry.tag === current ? "unchanged" : "changed";
|
|
159
|
+
lines.push(`- ${shown}: ${entry.agent.label()}, ${ago(now - entry.at)}, file ${state} since their edit.`);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (recording) {
|
|
163
|
+
bytes ??= await node_fs_promises.default.readFile(real).catch(() => void 0);
|
|
164
|
+
if (bytes !== void 0) record(ledger, real, agent, (0, _agimon_ai_po_hashline_files.computeFileTag)(bytes), now);
|
|
165
|
+
}
|
|
70
166
|
}
|
|
167
|
+
if (lines.length === 0) return void 0;
|
|
168
|
+
return [...call.content, {
|
|
169
|
+
type: "text",
|
|
170
|
+
text: renderNote(lines)
|
|
171
|
+
}];
|
|
71
172
|
}
|
|
72
|
-
/**
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
173
|
+
/** Edit presence for one hub session and the in-process children it spawns. */
|
|
174
|
+
function createSessionPresence(options) {
|
|
175
|
+
const { sessionId, ledger } = options;
|
|
176
|
+
const sessionAgent = {
|
|
177
|
+
key: `session:${sessionId}`,
|
|
178
|
+
label: () => `session ${sessionId}${options.workflowStep ? " (workflow step)" : ""}`,
|
|
179
|
+
contact: sessionId,
|
|
180
|
+
isLive: options.isLive
|
|
181
|
+
};
|
|
182
|
+
const safely = async (work) => {
|
|
183
|
+
try {
|
|
184
|
+
return await work();
|
|
185
|
+
} catch (error) {
|
|
186
|
+
options.onError?.(error);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
toolResult: {
|
|
192
|
+
event: "tool_result",
|
|
193
|
+
async handle(event, execution) {
|
|
194
|
+
const toolName = event.toolName;
|
|
195
|
+
if (execution.sessionId !== sessionId || typeof toolName !== "string" || !Array.isArray(event.content)) return;
|
|
196
|
+
const content = event.content;
|
|
197
|
+
const next = await safely(() => observePresence(ledger, sessionAgent, {
|
|
198
|
+
toolName,
|
|
199
|
+
args: event.args,
|
|
200
|
+
content,
|
|
201
|
+
isError: event.isError === true,
|
|
202
|
+
cwd: execution.cwd
|
|
203
|
+
}));
|
|
204
|
+
return next === void 0 ? void 0 : { content: next };
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
childHooks: { bind(scope) {
|
|
208
|
+
const { runId, parentSessionId, agent } = scope.request;
|
|
209
|
+
const child = {
|
|
210
|
+
key: `run:${runId}`,
|
|
211
|
+
label: () => `${options.labelFor(runId) ?? `${agent} (run ${runId.slice(0, SHORT_ID_LENGTH)})`} (subagent of session ${parentSessionId})`,
|
|
212
|
+
contact: parentSessionId,
|
|
213
|
+
isLive: () => !scope.signal.aborted
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
beforeTool: () => void 0,
|
|
217
|
+
async afterTool(event) {
|
|
218
|
+
const next = await safely(() => observePresence(ledger, child, {
|
|
219
|
+
toolName: event.toolName,
|
|
220
|
+
args: event.args,
|
|
221
|
+
content: event.content,
|
|
222
|
+
isError: event.isError,
|
|
223
|
+
cwd: scope.request.cwd || options.cwd
|
|
224
|
+
}));
|
|
225
|
+
return next === void 0 ? void 0 : { content: next };
|
|
226
|
+
},
|
|
227
|
+
dispose: async () => forgetPresence(ledger, child.key)
|
|
228
|
+
};
|
|
229
|
+
} },
|
|
230
|
+
dispose: () => forgetPresence(ledger, sessionAgent.key)
|
|
231
|
+
};
|
|
77
232
|
}
|
|
78
233
|
//#endregion
|
|
79
234
|
//#region src/services/lineDiff/index.ts
|
|
@@ -523,7 +678,7 @@ var EditTracker = class {
|
|
|
523
678
|
//#endregion
|
|
524
679
|
//#region src/services/fileEditPaths/index.ts
|
|
525
680
|
/** The directory this package owns, under whichever root holds agent state. */
|
|
526
|
-
const STATE_DIRECTORY = "
|
|
681
|
+
const STATE_DIRECTORY = "po-file-edit";
|
|
527
682
|
function hash(value) {
|
|
528
683
|
return (0, node_crypto.createHash)("sha256").update(value).digest("hex").slice(0, 16);
|
|
529
684
|
}
|
|
@@ -541,7 +696,7 @@ var FileEditPaths = class {
|
|
|
541
696
|
}
|
|
542
697
|
/**
|
|
543
698
|
* Where every session's state lives: the Pi agent directory, the same root
|
|
544
|
-
* every other
|
|
699
|
+
* every other Po package writes to, and never inside the working tree.
|
|
545
700
|
*
|
|
546
701
|
* An earlier layout put this in the repository's git common directory. That
|
|
547
702
|
* is git's own storage: nothing in git's lifecycle prunes a directory we add
|
|
@@ -571,31 +726,6 @@ var FileEditPaths = class {
|
|
|
571
726
|
snapshotsPath(cwd, sessionKey) {
|
|
572
727
|
return `${this.timelinePath(cwd, sessionKey).replace(/\.jsonl$/u, "")}.blobs`;
|
|
573
728
|
}
|
|
574
|
-
legacyStateDirectory(cwd) {
|
|
575
|
-
const common = this.gitCommonDirectory(cwd);
|
|
576
|
-
return common === void 0 ? void 0 : node_path.default.join(common, STATE_DIRECTORY);
|
|
577
|
-
}
|
|
578
|
-
/** Only still read so the state an older build left behind can be cleared away. */
|
|
579
|
-
gitCommonDirectory(cwd) {
|
|
580
|
-
try {
|
|
581
|
-
return (0, node_child_process.execFileSync)("git", [
|
|
582
|
-
"rev-parse",
|
|
583
|
-
"--path-format=absolute",
|
|
584
|
-
"--git-common-dir"
|
|
585
|
-
], {
|
|
586
|
-
cwd,
|
|
587
|
-
encoding: "utf8",
|
|
588
|
-
stdio: [
|
|
589
|
-
"ignore",
|
|
590
|
-
"pipe",
|
|
591
|
-
"ignore"
|
|
592
|
-
]
|
|
593
|
-
}).trim() || void 0;
|
|
594
|
-
} catch (error) {
|
|
595
|
-
if (error instanceof Error) return void 0;
|
|
596
|
-
throw error;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
729
|
};
|
|
600
730
|
//#endregion
|
|
601
731
|
//#region src/services/gitStatus/index.ts
|
|
@@ -683,6 +813,41 @@ var NodeGitStatusAdapter = class {
|
|
|
683
813
|
}
|
|
684
814
|
}
|
|
685
815
|
};
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/services/poIgnore/index.ts
|
|
818
|
+
function matchablePath(relPath) {
|
|
819
|
+
const normalized = relPath.replace(/\\/gu, "/");
|
|
820
|
+
if (normalized.length === 0 || normalized.includes("\0") || normalized === ".." || normalized.startsWith("../") || normalized.startsWith("/") || /^[A-Za-z]:\//u.test(normalized)) return;
|
|
821
|
+
return normalized;
|
|
822
|
+
}
|
|
823
|
+
/**
|
|
824
|
+
* Builds the project's ignore test, or nothing when there are no rules.
|
|
825
|
+
*
|
|
826
|
+
* Shared because the same rules are needed twice: once when deciding what to
|
|
827
|
+
* record, and once when deciding what to list. A caller that built its own
|
|
828
|
+
* matcher would let the two drift apart.
|
|
829
|
+
*
|
|
830
|
+
* The test answers false for anything it cannot safely match, so a path that
|
|
831
|
+
* escapes the project stays visible rather than being hidden by accident.
|
|
832
|
+
*/
|
|
833
|
+
function createPoIgnoreMatcher(content) {
|
|
834
|
+
try {
|
|
835
|
+
if (content.trim().length === 0) return void 0;
|
|
836
|
+
const matcher = (0, ignore.default)().add(content);
|
|
837
|
+
return (relPath) => {
|
|
838
|
+
const candidate = matchablePath(relPath);
|
|
839
|
+
return candidate !== void 0 && matcher.ignores(candidate);
|
|
840
|
+
};
|
|
841
|
+
} catch {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
/** Applies project-local gitignore rules while keeping unsafe candidates visible. */
|
|
846
|
+
function filterPoIgnoredFiles(items, content) {
|
|
847
|
+
const ignored = createPoIgnoreMatcher(content);
|
|
848
|
+
if (ignored === void 0) return items;
|
|
849
|
+
return items.filter((item) => !ignored(item.relPath));
|
|
850
|
+
}
|
|
686
851
|
/** How much of a file is read before deciding it is binary. */
|
|
687
852
|
const BINARY_SAMPLE_BYTES$1 = 8192;
|
|
688
853
|
const MISSING_CODES = /* @__PURE__ */ new Set([
|
|
@@ -1112,7 +1277,7 @@ var NodeTreeManifestAdapter = class {
|
|
|
1112
1277
|
/** The frame type the hub publishes and the web plugin claims; declared in the poWeb block too. */
|
|
1113
1278
|
const filesChannelType = "file_edits";
|
|
1114
1279
|
/** Footer status key whose presence shows the activity group; the extension publishes it. */
|
|
1115
|
-
const filesStatusKey = "
|
|
1280
|
+
const filesStatusKey = "po-file-edit-files";
|
|
1116
1281
|
//#endregion
|
|
1117
1282
|
//#region src/types/apiRoutes.ts
|
|
1118
1283
|
/**
|
|
@@ -1399,6 +1564,13 @@ function createFileEditsApi(options = {}) {
|
|
|
1399
1564
|
const root = node_path.default.resolve(cwd);
|
|
1400
1565
|
return isInside(root, filePath) && await isRealPathInside(root, filePath);
|
|
1401
1566
|
};
|
|
1567
|
+
/** The Po-owned folder holding the file, checked the same two ways as the working directory. */
|
|
1568
|
+
const readableRootOf = async (filePath) => {
|
|
1569
|
+
for (const root of options.readableRoots?.() ?? []) {
|
|
1570
|
+
const resolved = node_path.default.resolve(root);
|
|
1571
|
+
if (isInside(resolved, filePath) && await isRealPathInside(resolved, filePath)) return resolved;
|
|
1572
|
+
}
|
|
1573
|
+
};
|
|
1402
1574
|
app.get(routes.detail.path, async (context) => {
|
|
1403
1575
|
const requested = context.req.query(PATH_QUERY_PARAM);
|
|
1404
1576
|
const versions = await recordedVersions(requested);
|
|
@@ -1431,10 +1603,11 @@ function createFileEditsApi(options = {}) {
|
|
|
1431
1603
|
if (cwd === "") return context.json({ error: "This session has no working directory to read from." }, 403);
|
|
1432
1604
|
const root = node_path.default.resolve(cwd);
|
|
1433
1605
|
const filePath = node_path.default.resolve(root, requested);
|
|
1434
|
-
|
|
1606
|
+
const inSession = await isInsideSession(filePath);
|
|
1607
|
+
if (!inSession && await readableRootOf(filePath) === void 0) return context.json({ error: "The path leaves the session directory." }, 403);
|
|
1435
1608
|
const body = {
|
|
1436
1609
|
path: filePath,
|
|
1437
|
-
relPath: node_path.default.relative(root, filePath),
|
|
1610
|
+
relPath: inSession ? node_path.default.relative(root, filePath) : filePath,
|
|
1438
1611
|
working: await readWorking(filePath)
|
|
1439
1612
|
};
|
|
1440
1613
|
return context.json(body);
|
|
@@ -1478,10 +1651,15 @@ function createFileEditsApi(options = {}) {
|
|
|
1478
1651
|
const api = {
|
|
1479
1652
|
basePath: API_BASE_PATH,
|
|
1480
1653
|
start(context) {
|
|
1654
|
+
const environment = context.environment ?? process.env;
|
|
1655
|
+
const cwd = context.cwd;
|
|
1481
1656
|
const app = createFileEditsApi({
|
|
1482
1657
|
paths: new FileEditPaths(context.environment),
|
|
1483
1658
|
...context.sessionId === void 0 ? {} : { sessionId: context.sessionId },
|
|
1484
|
-
...
|
|
1659
|
+
...cwd === void 0 ? {} : {
|
|
1660
|
+
cwd,
|
|
1661
|
+
readableRoots: () => plansRoots(cwd, environment)
|
|
1662
|
+
}
|
|
1485
1663
|
});
|
|
1486
1664
|
return {
|
|
1487
1665
|
fetch: (request) => app.fetch(request),
|
|
@@ -1489,6 +1667,20 @@ const api = {
|
|
|
1489
1667
|
};
|
|
1490
1668
|
}
|
|
1491
1669
|
};
|
|
1670
|
+
/**
|
|
1671
|
+
* The plans directory Plan mode writes to, resolved the way Plan resolves it, so
|
|
1672
|
+
* a plan link in the transcript opens in the file view. A config that cannot be
|
|
1673
|
+
* read admits nothing extra rather than failing the preview.
|
|
1674
|
+
*/
|
|
1675
|
+
function plansRoots(cwd, environment) {
|
|
1676
|
+
const home = environment.HOME ?? node_os.default.homedir();
|
|
1677
|
+
try {
|
|
1678
|
+
const configured = (0, _agimon_ai_po_config.loadPoConfig)(cwd, home, environment).modes?.planning?.plansDirectory;
|
|
1679
|
+
return [(0, _agimon_ai_po_config.resolvePlanningPlansDirectory)(configured, cwd, home)];
|
|
1680
|
+
} catch {
|
|
1681
|
+
return [];
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1492
1684
|
//#endregion
|
|
1493
1685
|
//#region src/services/sessionStateSweep/index.ts
|
|
1494
1686
|
/**
|
|
@@ -1572,8 +1764,9 @@ async function sweepSessionState(options) {
|
|
|
1572
1764
|
//#endregion
|
|
1573
1765
|
//#region src/services/webFilesChannel/index.ts
|
|
1574
1766
|
/**
|
|
1575
|
-
* The changed-files channel consumes snapshots emitted by the owning session
|
|
1576
|
-
*
|
|
1767
|
+
* The changed-files channel consumes snapshots emitted by the owning session's
|
|
1768
|
+
* server plugin. Durable timeline reads seed a channel that starts after session
|
|
1769
|
+
* startup.
|
|
1577
1770
|
*/
|
|
1578
1771
|
/** Whether the file is still there to be opened. */
|
|
1579
1772
|
function stillExists(filePath) {
|
|
@@ -1610,7 +1803,7 @@ function readSessionFiles(timelinePath, cwd) {
|
|
|
1610
1803
|
diffable: isDiffable(foldVersions(events, entry.path))
|
|
1611
1804
|
}));
|
|
1612
1805
|
try {
|
|
1613
|
-
return
|
|
1806
|
+
return filterPoIgnoredFiles(items, node_fs.default.readFileSync(node_path.default.join(cwd, ".poignore"), "utf8"));
|
|
1614
1807
|
} catch {
|
|
1615
1808
|
return items;
|
|
1616
1809
|
}
|
|
@@ -1666,10 +1859,10 @@ function createFilesChannel() {
|
|
|
1666
1859
|
}
|
|
1667
1860
|
//#endregion
|
|
1668
1861
|
//#region src/services/fileEditSession/index.ts
|
|
1669
|
-
const
|
|
1670
|
-
function
|
|
1862
|
+
const PO_IGNORE_FILE = ".poignore";
|
|
1863
|
+
function readPoIgnore(cwd) {
|
|
1671
1864
|
try {
|
|
1672
|
-
const matcher =
|
|
1865
|
+
const matcher = createPoIgnoreMatcher(node_fs.default.readFileSync(node_path.default.join(cwd, PO_IGNORE_FILE), "utf8"));
|
|
1673
1866
|
return matcher === void 0 ? void 0 : (filePath) => matcher(node_path.default.relative(cwd, filePath));
|
|
1674
1867
|
} catch {
|
|
1675
1868
|
return;
|
|
@@ -1686,10 +1879,10 @@ function createFileEditSession({ agent, host }) {
|
|
|
1686
1879
|
const timeline = new TimelineStore();
|
|
1687
1880
|
const snapshots = new NodeSnapshotStoreAdapter();
|
|
1688
1881
|
const editTracker = new EditTracker(timeline, snapshots, new NodeTreeManifestAdapter(), { git: new NodeGitStatusAdapter() });
|
|
1689
|
-
if (host.context.directEvents === void 0) throw new Error("File-edit
|
|
1882
|
+
if (host.context.directEvents === void 0) throw new Error("File-edit server plugin requires the session direct event bus.");
|
|
1690
1883
|
const directEvents = host.context.directEvents;
|
|
1691
|
-
const telemetry = (0, _agimon_ai_po_telemetry.
|
|
1692
|
-
serviceName: "
|
|
1884
|
+
const telemetry = (0, _agimon_ai_po_telemetry.createPoTelemetry)({
|
|
1885
|
+
serviceName: "po-file-edit",
|
|
1693
1886
|
packageName: "@agimon-ai/po-file-edit",
|
|
1694
1887
|
env: environment,
|
|
1695
1888
|
enableLogs: true,
|
|
@@ -1712,7 +1905,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1712
1905
|
/**
|
|
1713
1906
|
* The channel carries the rows; the status decides whether the group exists.
|
|
1714
1907
|
*
|
|
1715
|
-
* A cockpit drives this
|
|
1908
|
+
* A cockpit drives this server plugin and never the Pi runtime, so without the status
|
|
1716
1909
|
* the dock had the file list on the wire and no frame to draw it in. The empty
|
|
1717
1910
|
* string is deliberate: the group declares `hideWhenEmpty`, so a session that
|
|
1718
1911
|
* has edited nothing publishes a key with no content and stays hidden.
|
|
@@ -1738,20 +1931,19 @@ function createFileEditSession({ agent, host }) {
|
|
|
1738
1931
|
const dispose = () => {
|
|
1739
1932
|
if (disposal) return disposal;
|
|
1740
1933
|
closed = true;
|
|
1741
|
-
disposal = (
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
if (lastRuntime)
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
})();
|
|
1934
|
+
disposal = (0, _agimon_ai_po_core_extensionFile.runCleanups)([
|
|
1935
|
+
stopActivity,
|
|
1936
|
+
async () => {
|
|
1937
|
+
if (lastRuntime && !environment[_agimon_ai_po_core_childProcess.SUBAGENT_CHILD_ENV]) await timeline.clear();
|
|
1938
|
+
},
|
|
1939
|
+
async () => {
|
|
1940
|
+
if (lastRuntime && !environment[_agimon_ai_po_core_childProcess.SUBAGENT_CHILD_ENV]) await snapshots.clear();
|
|
1941
|
+
},
|
|
1942
|
+
async () => {
|
|
1943
|
+
if (lastRuntime) await telemetry.recordEvent("po_file_edit.timeline_finished", { outcome: "closed" });
|
|
1944
|
+
},
|
|
1945
|
+
() => telemetry.shutdown()
|
|
1946
|
+
], "File-edit session cleanup failed.");
|
|
1755
1947
|
return disposal;
|
|
1756
1948
|
};
|
|
1757
1949
|
return {
|
|
@@ -1769,7 +1961,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1769
1961
|
const snapshotsPath = paths.snapshotsPath(executionContext.cwd, sessionKey);
|
|
1770
1962
|
timeline.initialize(timelinePath);
|
|
1771
1963
|
snapshots.initialize(snapshotsPath);
|
|
1772
|
-
const isIgnored =
|
|
1964
|
+
const isIgnored = readPoIgnore(executionContext.cwd);
|
|
1773
1965
|
editTracker.reset({
|
|
1774
1966
|
exclude: [
|
|
1775
1967
|
timelinePath,
|
|
@@ -1792,16 +1984,11 @@ function createFileEditSession({ agent, host }) {
|
|
|
1792
1984
|
directory: paths.stateDirectory(),
|
|
1793
1985
|
keep: timelinePath
|
|
1794
1986
|
});
|
|
1795
|
-
const legacy = paths.legacyStateDirectory(executionContext.cwd);
|
|
1796
|
-
if (legacy !== void 0 && legacy !== paths.stateDirectory()) await sweepSessionState({
|
|
1797
|
-
directory: legacy,
|
|
1798
|
-
removeDirectory: true
|
|
1799
|
-
});
|
|
1800
1987
|
} catch {}
|
|
1801
1988
|
});
|
|
1802
1989
|
if (!initialized) {
|
|
1803
1990
|
initialized = true;
|
|
1804
|
-
await track(() => telemetry.recordEvent("
|
|
1991
|
+
await track(() => telemetry.recordEvent("po_file_edit.timeline_started", { outcome: "initialized" }));
|
|
1805
1992
|
}
|
|
1806
1993
|
return () => active === runtime ? stopActivity() : Promise.resolve();
|
|
1807
1994
|
}
|
|
@@ -1818,7 +2005,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1818
2005
|
const hookStartedAt = Date.now();
|
|
1819
2006
|
await editTracker.start(toolCallId, toolName, event.args, executionContext.cwd);
|
|
1820
2007
|
if (active !== runtime) return;
|
|
1821
|
-
await telemetry.recordEvent("
|
|
2008
|
+
await telemetry.recordEvent("po_file_edit.edit_started", {
|
|
1822
2009
|
"tool.name": toolName,
|
|
1823
2010
|
hook_ms: Date.now() - hookStartedAt
|
|
1824
2011
|
});
|
|
@@ -1839,7 +2026,7 @@ function createFileEditSession({ agent, host }) {
|
|
|
1839
2026
|
await editTracker.end(toolCallId, isError, executionContext.cwd);
|
|
1840
2027
|
if (active !== runtime) return;
|
|
1841
2028
|
publish(runtime);
|
|
1842
|
-
await telemetry.recordEvent("
|
|
2029
|
+
await telemetry.recordEvent("po_file_edit.edit_finished", {
|
|
1843
2030
|
"tool.name": toolName,
|
|
1844
2031
|
"tool.result.error": isError,
|
|
1845
2032
|
...startedAt === void 0 ? {} : { duration_ms: Date.now() - startedAt },
|
|
@@ -1854,12 +2041,38 @@ function createFileEditSession({ agent, host }) {
|
|
|
1854
2041
|
}
|
|
1855
2042
|
//#endregion
|
|
1856
2043
|
//#region src/extensions/workspaces/sessions/(backend)/_lib/serverRoot.ts
|
|
2044
|
+
function createPresence({ agent, host, context }) {
|
|
2045
|
+
const ledger = host.context.editPresence;
|
|
2046
|
+
if (!agent || !ledger) return void 0;
|
|
2047
|
+
const execution = agent.context;
|
|
2048
|
+
return createSessionPresence({
|
|
2049
|
+
ledger,
|
|
2050
|
+
sessionId: execution.sessionId,
|
|
2051
|
+
cwd: execution.cwd,
|
|
2052
|
+
workflowStep: execution.sessionContext?.provenance === _agimon_ai_po_core_hubChannel.WORKFLOW_STEP_SESSION_PROVENANCE,
|
|
2053
|
+
isLive: () => host.context.sessionService?.isLive(execution.sessionId) ?? true,
|
|
2054
|
+
labelFor: (runId) => (0, _agimon_ai_po_core_childSession.readPoChildSessionLabels)(context)?.labelFor(runId),
|
|
2055
|
+
onError: (error) => host.context.onNotice(`Edit presence failed: ${error instanceof Error ? error.message : String(error)}`)
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
1857
2058
|
const createFileEditServerRoot = (context) => {
|
|
1858
2059
|
const runtime = createFileEditSession(context);
|
|
2060
|
+
const presence = createPresence(context);
|
|
1859
2061
|
return {
|
|
1860
|
-
value:
|
|
2062
|
+
value: {
|
|
2063
|
+
...runtime,
|
|
2064
|
+
...presence === void 0 ? {} : { presence }
|
|
2065
|
+
},
|
|
1861
2066
|
activities: runtime.activities,
|
|
1862
|
-
|
|
2067
|
+
...presence === void 0 ? {} : { services: [(cordis) => {
|
|
2068
|
+
cordis.inject([_agimon_ai_po_core_childSession.PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE], (registryContext) => {
|
|
2069
|
+
const registration = registryContext.get(_agimon_ai_po_core_childSession.PO_CHILD_SESSION_HOOK_REGISTRY_SERVICE).register(presence.childHooks);
|
|
2070
|
+
return () => registration.dispose();
|
|
2071
|
+
});
|
|
2072
|
+
}] },
|
|
2073
|
+
onDispose: async (pluginContext) => {
|
|
2074
|
+
await (0, _agimon_ai_po_core_extensionFile.runCleanups)([() => presence?.dispose(), () => runtime.onDispose?.(pluginContext)], "File-edit root cleanup failed.");
|
|
2075
|
+
}
|
|
1863
2076
|
};
|
|
1864
2077
|
};
|
|
1865
2078
|
//#endregion
|
|
@@ -1888,23 +2101,32 @@ var tool_execution_end_server_default = (0, _agimon_ai_po_core_extensionFile.def
|
|
|
1888
2101
|
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-execution-start.server.ts
|
|
1889
2102
|
var tool_execution_start_server_default = (0, _agimon_ai_po_core_extensionFile.defineHook)((context) => context.root.hooks[0]);
|
|
1890
2103
|
//#endregion
|
|
2104
|
+
//#region src/extensions/workspaces/sessions/(backend)/hook/tool-result.server.ts
|
|
2105
|
+
var tool_result_server_default = (0, _agimon_ai_po_core_extensionFile.defineHook)(({ root }) => ({
|
|
2106
|
+
event: "tool_result",
|
|
2107
|
+
handle: async (event, execution) => root.presence?.toolResult.handle(event, execution)
|
|
2108
|
+
}));
|
|
2109
|
+
//#endregion
|
|
1891
2110
|
//#region generated/server.ts
|
|
1892
2111
|
const at = (value, context) => typeof value === "function" ? value(context) : value;
|
|
2112
|
+
/* v8 ignore next -- generated normalization branch */
|
|
2113
|
+
const defined = (values) => values.filter((value) => value !== void 0);
|
|
1893
2114
|
const via = (identity, value) => value === void 0 ? void 0 : {
|
|
1894
2115
|
...identity,
|
|
1895
2116
|
...value
|
|
1896
2117
|
};
|
|
1897
|
-
const
|
|
2118
|
+
const serverPlugin = (0, _agimon_ai_po_core_serverPlugin.defineServerPlugin)({
|
|
1898
2119
|
name: "@agimon-ai/po-file-edit",
|
|
1899
|
-
providerOverrides: (0,
|
|
2120
|
+
providerOverrides: (0, _agimon_ai_po_core_serverPlugin.composeServerProviderOverrides)(root_server_default),
|
|
2121
|
+
injects: (0, _agimon_ai_po_core_extensionFile.composeRootInjects)({ session: [root_server_default] }),
|
|
1900
2122
|
global: () => ({ get channels() {
|
|
1901
2123
|
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
1902
2124
|
} }),
|
|
1903
|
-
workspace: () => ({ get channels() {
|
|
1904
|
-
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
2125
|
+
workspace: (context) => ({ get channels() {
|
|
2126
|
+
return defined([context.hosted("global") ? void 0 : () => via({ frameType: "file_edits" }, fileEdits_server_default())]);
|
|
1905
2127
|
} }),
|
|
1906
2128
|
session: async (context) => {
|
|
1907
|
-
const scopeSession = await root_server_default
|
|
2129
|
+
const scopeSession = await context.scopeRoot("session", root_server_default, context);
|
|
1908
2130
|
const contextSession = {
|
|
1909
2131
|
...context,
|
|
1910
2132
|
root: scopeSession.value
|
|
@@ -1915,10 +2137,14 @@ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
|
|
|
1915
2137
|
return [at(route_server_default, contextSession)];
|
|
1916
2138
|
},
|
|
1917
2139
|
get channels() {
|
|
1918
|
-
return [() => via({ frameType: "file_edits" }, fileEdits_server_default())];
|
|
2140
|
+
return defined([context.hosted("global") ? void 0 : () => via({ frameType: "file_edits" }, fileEdits_server_default())]);
|
|
1919
2141
|
},
|
|
1920
2142
|
get hooks() {
|
|
1921
|
-
return [
|
|
2143
|
+
return [
|
|
2144
|
+
via({ event: "tool_execution_end" }, at(tool_execution_end_server_default, contextSession)),
|
|
2145
|
+
via({ event: "tool_execution_start" }, at(tool_execution_start_server_default, contextSession)),
|
|
2146
|
+
via({ event: "tool_result" }, at(tool_result_server_default, contextSession))
|
|
2147
|
+
];
|
|
1922
2148
|
},
|
|
1923
2149
|
services: [...scopeSession.services ?? []],
|
|
1924
2150
|
get activities() {
|
|
@@ -1928,7 +2154,7 @@ const facet = (0, _agimon_ai_po_core_serverFacet.defineServerPlugin)({
|
|
|
1928
2154
|
}
|
|
1929
2155
|
});
|
|
1930
2156
|
//#endregion
|
|
1931
|
-
exports.default =
|
|
1932
|
-
exports.
|
|
2157
|
+
exports.default = serverPlugin;
|
|
2158
|
+
exports.serverPlugin = serverPlugin;
|
|
1933
2159
|
|
|
1934
2160
|
//# sourceMappingURL=server.cjs.map
|