@halofy/agent-connect 0.5.1 → 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/README.md +15 -1
- package/package.json +2 -2
- package/src/claude-hook.mjs +20 -0
- package/src/client-registry.mjs +9 -2
- package/src/host-config.mjs +3 -2
- package/src/host-hook.mjs +37 -3
- package/src/host-roots.mjs +13 -0
- package/src/install.mjs +71 -0
- package/src/installer-cli.mjs +305 -22
- package/src/runtime.mjs +14 -9
- package/src/session.mjs +10 -6
- package/src/skills-sync.mjs +266 -0
- package/src/transcript-drivers/claude.mjs +33 -0
- package/src/transcript-drivers/codex.mjs +188 -0
- package/src/transcript-drivers/index.mjs +18 -0
- package/src/transcript-drivers/kimi.mjs +180 -0
- package/src/transcript-drivers/shared.mjs +104 -0
- package/src/transport.mjs +7 -0
- package/src/version.mjs +3 -3
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { open, realpath } from "node:fs/promises";
|
|
2
|
+
import { resolve, sep } from "node:path";
|
|
3
|
+
|
|
4
|
+
// Windowed suffix budget per catch-up call. Successive calls drain the
|
|
5
|
+
// remainder; nothing is skipped, memory stays bounded even for giant files.
|
|
6
|
+
export const MAX_DRIVER_SUFFIX_BYTES = 8 * 1024 * 1024;
|
|
7
|
+
|
|
8
|
+
// Host session ids are interpolated into filesystem lookups. Anything with a
|
|
9
|
+
// path character is refused outright — no capture beats a contained read of
|
|
10
|
+
// the wrong file (README containment commitment).
|
|
11
|
+
const HOST_SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;
|
|
12
|
+
|
|
13
|
+
export function boundedHostSessionId(value) {
|
|
14
|
+
const text = String(value ?? "");
|
|
15
|
+
return HOST_SESSION_ID_PATTERN.test(text) ? text : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve `candidate` and require it to live under `root` after every
|
|
20
|
+
* symlink is followed. Index files and cached paths are untrusted input;
|
|
21
|
+
* any escape resolves to null (no capture), never an error.
|
|
22
|
+
*/
|
|
23
|
+
export async function containedPath(root, candidate) {
|
|
24
|
+
try {
|
|
25
|
+
const realRoot = await realpath(root);
|
|
26
|
+
const resolved = resolve(realRoot, String(candidate ?? ""));
|
|
27
|
+
if (resolved !== realRoot && !resolved.startsWith(realRoot + sep)) return null;
|
|
28
|
+
const real = await realpath(resolved);
|
|
29
|
+
if (real !== realRoot && !real.startsWith(realRoot + sep)) return null;
|
|
30
|
+
return real;
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Positioned read of at most `maxBytes` starting at `byteOffset`. */
|
|
37
|
+
export async function readSuffixWindow(path, byteOffset, maxBytes = MAX_DRIVER_SUFFIX_BYTES) {
|
|
38
|
+
const handle = await open(path, "r");
|
|
39
|
+
try {
|
|
40
|
+
const size = (await handle.stat()).size;
|
|
41
|
+
const start = Number.isSafeInteger(byteOffset) && byteOffset >= 0 && byteOffset <= size
|
|
42
|
+
? byteOffset : 0;
|
|
43
|
+
const length = Math.min(size - start, maxBytes);
|
|
44
|
+
const buffer = Buffer.alloc(length);
|
|
45
|
+
if (length > 0) await handle.read(buffer, 0, length, start);
|
|
46
|
+
return { buffer, start };
|
|
47
|
+
} finally {
|
|
48
|
+
await handle.close();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Yield `{ entry, byteOffset, endOffset }` for every complete JSONL line in
|
|
54
|
+
* the window. An unparseable line yields `entry: null` (drivers skip it —
|
|
55
|
+
* unlike the Claude archive, an ignored line is the norm for selective
|
|
56
|
+
* usage/metadata readers). A trailing record without its newline is left
|
|
57
|
+
* unread for the next catch-up.
|
|
58
|
+
*/
|
|
59
|
+
export function* completeJsonlLines(buffer, start) {
|
|
60
|
+
let position = 0;
|
|
61
|
+
while (position < buffer.length) {
|
|
62
|
+
const newline = buffer.indexOf(0x0a, position);
|
|
63
|
+
if (newline === -1) break;
|
|
64
|
+
let entry = null;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = JSON.parse(buffer.subarray(position, newline).toString("utf8"));
|
|
67
|
+
entry = parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
68
|
+
} catch {
|
|
69
|
+
entry = null;
|
|
70
|
+
}
|
|
71
|
+
yield { entry, byteOffset: start + position, endOffset: start + newline + 1 };
|
|
72
|
+
position = newline + 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* End offset for a drained window. If a full window held no newline at all
|
|
78
|
+
* (pathological single-line file), advance past it rather than re-reading the
|
|
79
|
+
* same bytes forever — usage lines are tiny, so nothing real is lost.
|
|
80
|
+
*/
|
|
81
|
+
export function drainedEndOffset(lastEndOffset, start, bufferLength, maxBytes) {
|
|
82
|
+
if (lastEndOffset > start) return lastEndOffset;
|
|
83
|
+
if (bufferLength >= maxBytes) return start + bufferLength;
|
|
84
|
+
return start;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const MAX_HOST_STATE_BYTES = 1024;
|
|
88
|
+
|
|
89
|
+
/** Bound the per-session driver scratch state persisted on the cursor. */
|
|
90
|
+
export function boundedHostState(value) {
|
|
91
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
92
|
+
const serialized = JSON.stringify(value);
|
|
93
|
+
return Buffer.byteLength(serialized, "utf8") <= MAX_HOST_STATE_BYTES ? value : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Bound for host-supplied metadata strings, mirroring collectClaudeMetadata:
|
|
98
|
+
* non-empty, capped, and free of control characters. The server rejects a
|
|
99
|
+
* whole batch over an oversized field, so bounding here is not cosmetic.
|
|
100
|
+
*/
|
|
101
|
+
export function boundedMetadataText(value, max) {
|
|
102
|
+
return typeof value === "string" && value.length > 0 && value.length <= max &&
|
|
103
|
+
!/[\u0000-\u001f\u007f]/.test(value) ? value : undefined;
|
|
104
|
+
}
|
package/src/transport.mjs
CHANGED
|
@@ -64,6 +64,13 @@ export class SignedRuntimeTransport {
|
|
|
64
64
|
});
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
/** Badge skill check-in beacon: exactly the installed {skillId, sha} pairs (D39). */
|
|
68
|
+
skillsCheckin(items) { return this.request("/v1/agent-runtime/skills/checkin", { body: { items } }); }
|
|
69
|
+
/** Governed skill export — the same gate as a console download (D38). */
|
|
70
|
+
skillDownload(skillId) {
|
|
71
|
+
return this.request(`/v1/agent-runtime/skills/${encodeURIComponent(skillId)}/download`, { method: "GET" });
|
|
72
|
+
}
|
|
73
|
+
|
|
67
74
|
openSession(body) { return this.request("/v1/agent-sessions/open", { body }); }
|
|
68
75
|
appendEvents(sessionId, events) { return this.request(`/v1/agent-sessions/${encodeURIComponent(sessionId)}/events`, { body: { events } }); }
|
|
69
76
|
recall(sessionId, body) { return this.request(`/v1/agent-sessions/${encodeURIComponent(sessionId)}/recall`, { body }); }
|
package/src/version.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "@halofy/agent-connect";
|
|
2
|
-
export const INSTALLER_VERSION = "0.
|
|
3
|
-
export const RUNTIME_VERSION = "0.
|
|
4
|
-
export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-
|
|
2
|
+
export const INSTALLER_VERSION = "0.7.0";
|
|
3
|
+
export const RUNTIME_VERSION = "0.7.0";
|
|
4
|
+
export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-03.1";
|