blun-king-cli 9.1.563 → 9.1.565
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/agent-spine-plugin/.claude-plugin/marketplace.json +20 -20
- package/agent-spine-plugin/.claude-plugin/plugin.json +14 -14
- package/agent-spine-plugin/.codex-plugin/plugin.json +34 -34
- package/agent-spine-plugin/.mcp.json +8 -8
- package/agent-spine-plugin/CONTRIBUTING.md +52 -0
- package/agent-spine-plugin/LICENSE +186 -186
- package/agent-spine-plugin/README.md +394 -0
- package/agent-spine-plugin/SECURITY.md +47 -0
- package/agent-spine-plugin/assets/agentspine-banner.svg +32 -32
- package/agent-spine-plugin/bin/agentspine-mcp.js +4 -4
- package/agent-spine-plugin/bin/agentspine.js +7 -7
- package/agent-spine-plugin/blun.plugin.json +33 -33
- package/agent-spine-plugin/hooks/codex.json +47 -47
- package/agent-spine-plugin/hooks/hooks.json +106 -106
- package/agent-spine-plugin/hooks/version.json +5 -5
- package/agent-spine-plugin/package.json +69 -69
- package/agent-spine-plugin/scripts/check-hosts.js +199 -199
- package/agent-spine-plugin/skill/SKILL.md +76 -76
- package/agent-spine-plugin/skills/agent-spine/SKILL.md +85 -85
- package/agent-spine-plugin/src/cli.js +1488 -1442
- package/agent-spine-plugin/src/hook.js +886 -812
- package/agent-spine-plugin/src/index.js +93 -93
- package/agent-spine-plugin/src/lib/acceptance.js +333 -333
- package/agent-spine-plugin/src/lib/attention.js +755 -755
- package/agent-spine-plugin/src/lib/audit.js +351 -342
- package/agent-spine-plugin/src/lib/authentication.js +515 -515
- package/agent-spine-plugin/src/lib/briefing.js +317 -317
- package/agent-spine-plugin/src/lib/catalog.js +167 -167
- package/agent-spine-plugin/src/lib/channel-runtime.js +665 -665
- package/agent-spine-plugin/src/lib/context.js +154 -154
- package/agent-spine-plugin/src/lib/continuity.js +338 -338
- package/agent-spine-plugin/src/lib/coordination.js +577 -577
- package/agent-spine-plugin/src/lib/documents.js +217 -217
- package/agent-spine-plugin/src/lib/feed-transport.js +499 -499
- package/agent-spine-plugin/src/lib/filesystem-retry.js +32 -32
- package/agent-spine-plugin/src/lib/gateway-runtime.js +1119 -760
- package/agent-spine-plugin/src/lib/graph.js +337 -337
- package/agent-spine-plugin/src/lib/hook-audit.js +29 -0
- package/agent-spine-plugin/src/lib/https-transport.js +392 -392
- package/agent-spine-plugin/src/lib/indexed-memory-offline.js +40 -40
- package/agent-spine-plugin/src/lib/indexed-memory.js +281 -281
- package/agent-spine-plugin/src/lib/learning.js +6923 -6466
- package/agent-spine-plugin/src/lib/object-transport.js +206 -206
- package/agent-spine-plugin/src/lib/owned-file-lock.js +143 -143
- package/agent-spine-plugin/src/lib/paths.js +109 -109
- package/agent-spine-plugin/src/lib/peer-transport.js +283 -283
- package/agent-spine-plugin/src/lib/persona-runtime.js +581 -581
- package/agent-spine-plugin/src/lib/preflight.js +702 -702
- package/agent-spine-plugin/src/lib/runtime.js +13 -13
- package/agent-spine-plugin/src/lib/selfstarter.js +892 -819
- package/agent-spine-plugin/src/lib/sharing.js +969 -969
- package/agent-spine-plugin/src/lib/source-roots.js +529 -482
- package/agent-spine-plugin/src/lib/sqlite-transport.js +501 -501
- package/agent-spine-plugin/src/lib/telegram-adapter.js +119 -119
- package/agent-spine-plugin/src/lib/voice-runtime.js +39 -39
- package/agent-spine-plugin/src/mcp.js +597 -572
- package/agent-spine-plugin/src/version.js +1 -1
- package/agent-spine-plugin/src/worker.js +202 -195
- package/bin/active-steer-priority-policy.cjs +24 -0
- package/bin/launcher-runtime.js +8 -1
- package/bin/mnemo-tool-agent-policy.cjs +22 -0
- package/bin/thinking-activity-status-policy.cjs +132 -0
- package/bin/thinking-only-guard.cjs +75 -0
- package/bin/tool-call-loop-policy.cjs +53 -0
- package/bin/turn-thinking-policy.cjs +25 -1
- package/blun.mjs +554 -64
- package/package.json +4 -1
- package/standard-skills/translate-native/LICENSE +21 -21
- package/standard-skills/translate-native/references/evaluation-protocol.md +95 -95
- package/standard-skills/translate-native/references/native-orthography.md +79 -79
- package/standard-skills/translate-native/references/native-translation-standard.md +94 -94
- package/standard-skills/translate-native/references/structured-content.md +72 -72
- package/standard-skills/translate-native/references/translationese-review.md +77 -77
- package/standard-skills/translate-native/scripts/blun_language_guard.py +697 -697
- package/standard-skills/translate-native/scripts/check_diacritics.py +353 -353
- package/standard-skills/translate-native/scripts/guard_service_client.py +264 -264
- package/standard-skills/translate-native/scripts/language_gateway.py +62 -62
- package/standard-skills/translate-native/scripts/language_quality.py +377 -377
- package/standard-skills/translate-native/scripts/pre_output_guard.py +64 -64
- package/standard-skills/translate-native/scripts/translation_guard.py +916 -916
- package/standard-tools/language-guard/blun_language_guard.py +697 -697
- package/standard-tools/language-guard/check_diacritics.py +353 -353
- package/standard-tools/language-guard/guard_service_client.py +264 -264
- package/standard-tools/language-guard/language_quality.py +377 -377
- package/standard-tools/language-guard/translation_guard.py +916 -916
|
@@ -1,143 +1,143 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { open, readFile, stat, unlink, utimes } from "node:fs/promises";
|
|
3
|
-
import { isFileLockContention, isTransientLockMetadataError } from "./filesystem-retry.js";
|
|
4
|
-
|
|
5
|
-
const LOCK_SCHEMA = "agentspine.owned-file-lock/v1";
|
|
6
|
-
|
|
7
|
-
function delay(milliseconds) {
|
|
8
|
-
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
function lockPayload(token, acquiredAt, leaseMs) {
|
|
12
|
-
return {
|
|
13
|
-
schema: LOCK_SCHEMA,
|
|
14
|
-
token,
|
|
15
|
-
acquiredAt,
|
|
16
|
-
leaseMs,
|
|
17
|
-
authority: "state-coordination-only"
|
|
18
|
-
};
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function readOwner(path) {
|
|
22
|
-
try {
|
|
23
|
-
const value = JSON.parse(await readFile(path, "utf8"));
|
|
24
|
-
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
25
|
-
|| value.schema !== LOCK_SCHEMA || typeof value.token !== "string") return null;
|
|
26
|
-
return value;
|
|
27
|
-
} catch (error) {
|
|
28
|
-
if (error.code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
29
|
-
throw error;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function sameFile(left, right) {
|
|
34
|
-
return left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
35
|
-
&& left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
async function removeStaleLock(path, staleAfterMs) {
|
|
39
|
-
let before;
|
|
40
|
-
try {
|
|
41
|
-
before = await stat(path);
|
|
42
|
-
} catch (error) {
|
|
43
|
-
if (isTransientLockMetadataError(error)) return false;
|
|
44
|
-
throw error;
|
|
45
|
-
}
|
|
46
|
-
if (Date.now() - before.mtimeMs <= staleAfterMs) return false;
|
|
47
|
-
await readOwner(path);
|
|
48
|
-
let after;
|
|
49
|
-
try {
|
|
50
|
-
after = await stat(path);
|
|
51
|
-
} catch (error) {
|
|
52
|
-
if (isTransientLockMetadataError(error)) return false;
|
|
53
|
-
throw error;
|
|
54
|
-
}
|
|
55
|
-
if (!sameFile(before, after) || Date.now() - after.mtimeMs <= staleAfterMs) return false;
|
|
56
|
-
try {
|
|
57
|
-
await unlink(path);
|
|
58
|
-
return true;
|
|
59
|
-
} catch (error) {
|
|
60
|
-
if (isFileLockContention(error) || isTransientLockMetadataError(error)) return false;
|
|
61
|
-
throw error;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export async function withOwnedFileLock(path, task, {
|
|
66
|
-
staleAfterMs = 15000,
|
|
67
|
-
heartbeatIntervalMs = 1000,
|
|
68
|
-
retryDelayMs = 25,
|
|
69
|
-
maxAttempts = 80
|
|
70
|
-
} = {}) {
|
|
71
|
-
if (typeof task !== "function") throw new Error("owned file lock requires a task");
|
|
72
|
-
if (!Number.isInteger(staleAfterMs) || staleAfterMs < 50
|
|
73
|
-
|| !Number.isInteger(heartbeatIntervalMs) || heartbeatIntervalMs < 10
|
|
74
|
-
|| heartbeatIntervalMs * 3 >= staleAfterMs
|
|
75
|
-
|| !Number.isInteger(retryDelayMs) || retryDelayMs < 1
|
|
76
|
-
|| !Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
77
|
-
throw new Error("owned file lock timing is invalid");
|
|
78
|
-
}
|
|
79
|
-
const token = randomUUID();
|
|
80
|
-
const acquiredAt = new Date().toISOString();
|
|
81
|
-
let acquired = false;
|
|
82
|
-
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
83
|
-
let handle;
|
|
84
|
-
try {
|
|
85
|
-
handle = await open(path, "wx", 0o600);
|
|
86
|
-
const payload = `${JSON.stringify(lockPayload(token, acquiredAt, staleAfterMs))}\n`;
|
|
87
|
-
await handle.writeFile(payload, "utf8");
|
|
88
|
-
acquired = true;
|
|
89
|
-
break;
|
|
90
|
-
} catch (error) {
|
|
91
|
-
if (!isFileLockContention(error)) {
|
|
92
|
-
if (handle) {
|
|
93
|
-
await handle.close();
|
|
94
|
-
handle = null;
|
|
95
|
-
await unlink(path).catch((cleanupError) => {
|
|
96
|
-
if (cleanupError.code !== "ENOENT") error.cleanupError = cleanupError;
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
throw error;
|
|
100
|
-
}
|
|
101
|
-
await removeStaleLock(path, staleAfterMs);
|
|
102
|
-
if (attempt + 1 < maxAttempts) await delay(retryDelayMs);
|
|
103
|
-
} finally {
|
|
104
|
-
await handle?.close();
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
if (!acquired) throw new Error("state is busy; retry shortly");
|
|
108
|
-
|
|
109
|
-
let ownershipError = null;
|
|
110
|
-
let heartbeat = Promise.resolve();
|
|
111
|
-
const assertOwned = async () => {
|
|
112
|
-
if (ownershipError) throw ownershipError;
|
|
113
|
-
const owner = await readOwner(path);
|
|
114
|
-
if (!owner || owner.token !== token) {
|
|
115
|
-
ownershipError = new Error("state lock ownership was lost; mutation aborted");
|
|
116
|
-
throw ownershipError;
|
|
117
|
-
}
|
|
118
|
-
};
|
|
119
|
-
const renew = async () => {
|
|
120
|
-
await assertOwned();
|
|
121
|
-
const now = new Date();
|
|
122
|
-
await utimes(path, now, now);
|
|
123
|
-
};
|
|
124
|
-
const timer = setInterval(() => {
|
|
125
|
-
heartbeat = heartbeat.then(renew).catch((error) => { ownershipError ||= error; });
|
|
126
|
-
}, heartbeatIntervalMs);
|
|
127
|
-
timer.unref?.();
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
const result = await task({ token, acquiredAt, assertOwned });
|
|
131
|
-
await assertOwned();
|
|
132
|
-
return result;
|
|
133
|
-
} finally {
|
|
134
|
-
clearInterval(timer);
|
|
135
|
-
await heartbeat;
|
|
136
|
-
const owner = await readOwner(path).catch(() => null);
|
|
137
|
-
if (owner?.token === token) {
|
|
138
|
-
await unlink(path).catch((error) => {
|
|
139
|
-
if (error.code !== "ENOENT") throw error;
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { open, readFile, stat, unlink, utimes } from "node:fs/promises";
|
|
3
|
+
import { isFileLockContention, isTransientLockMetadataError } from "./filesystem-retry.js";
|
|
4
|
+
|
|
5
|
+
const LOCK_SCHEMA = "agentspine.owned-file-lock/v1";
|
|
6
|
+
|
|
7
|
+
function delay(milliseconds) {
|
|
8
|
+
return new Promise((resolve) => setTimeout(resolve, milliseconds));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function lockPayload(token, acquiredAt, leaseMs) {
|
|
12
|
+
return {
|
|
13
|
+
schema: LOCK_SCHEMA,
|
|
14
|
+
token,
|
|
15
|
+
acquiredAt,
|
|
16
|
+
leaseMs,
|
|
17
|
+
authority: "state-coordination-only"
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readOwner(path) {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(await readFile(path, "utf8"));
|
|
24
|
+
if (!value || typeof value !== "object" || Array.isArray(value)
|
|
25
|
+
|| value.schema !== LOCK_SCHEMA || typeof value.token !== "string") return null;
|
|
26
|
+
return value;
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (error.code === "ENOENT" || error instanceof SyntaxError) return null;
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sameFile(left, right) {
|
|
34
|
+
return left.dev === right.dev && left.ino === right.ino && left.size === right.size
|
|
35
|
+
&& left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function removeStaleLock(path, staleAfterMs) {
|
|
39
|
+
let before;
|
|
40
|
+
try {
|
|
41
|
+
before = await stat(path);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
if (isTransientLockMetadataError(error)) return false;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
if (Date.now() - before.mtimeMs <= staleAfterMs) return false;
|
|
47
|
+
await readOwner(path);
|
|
48
|
+
let after;
|
|
49
|
+
try {
|
|
50
|
+
after = await stat(path);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (isTransientLockMetadataError(error)) return false;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
if (!sameFile(before, after) || Date.now() - after.mtimeMs <= staleAfterMs) return false;
|
|
56
|
+
try {
|
|
57
|
+
await unlink(path);
|
|
58
|
+
return true;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (isFileLockContention(error) || isTransientLockMetadataError(error)) return false;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function withOwnedFileLock(path, task, {
|
|
66
|
+
staleAfterMs = 15000,
|
|
67
|
+
heartbeatIntervalMs = 1000,
|
|
68
|
+
retryDelayMs = 25,
|
|
69
|
+
maxAttempts = 80
|
|
70
|
+
} = {}) {
|
|
71
|
+
if (typeof task !== "function") throw new Error("owned file lock requires a task");
|
|
72
|
+
if (!Number.isInteger(staleAfterMs) || staleAfterMs < 50
|
|
73
|
+
|| !Number.isInteger(heartbeatIntervalMs) || heartbeatIntervalMs < 10
|
|
74
|
+
|| heartbeatIntervalMs * 3 >= staleAfterMs
|
|
75
|
+
|| !Number.isInteger(retryDelayMs) || retryDelayMs < 1
|
|
76
|
+
|| !Number.isInteger(maxAttempts) || maxAttempts < 1) {
|
|
77
|
+
throw new Error("owned file lock timing is invalid");
|
|
78
|
+
}
|
|
79
|
+
const token = randomUUID();
|
|
80
|
+
const acquiredAt = new Date().toISOString();
|
|
81
|
+
let acquired = false;
|
|
82
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
83
|
+
let handle;
|
|
84
|
+
try {
|
|
85
|
+
handle = await open(path, "wx", 0o600);
|
|
86
|
+
const payload = `${JSON.stringify(lockPayload(token, acquiredAt, staleAfterMs))}\n`;
|
|
87
|
+
await handle.writeFile(payload, "utf8");
|
|
88
|
+
acquired = true;
|
|
89
|
+
break;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!isFileLockContention(error)) {
|
|
92
|
+
if (handle) {
|
|
93
|
+
await handle.close();
|
|
94
|
+
handle = null;
|
|
95
|
+
await unlink(path).catch((cleanupError) => {
|
|
96
|
+
if (cleanupError.code !== "ENOENT") error.cleanupError = cleanupError;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
await removeStaleLock(path, staleAfterMs);
|
|
102
|
+
if (attempt + 1 < maxAttempts) await delay(retryDelayMs);
|
|
103
|
+
} finally {
|
|
104
|
+
await handle?.close();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (!acquired) throw new Error("state is busy; retry shortly");
|
|
108
|
+
|
|
109
|
+
let ownershipError = null;
|
|
110
|
+
let heartbeat = Promise.resolve();
|
|
111
|
+
const assertOwned = async () => {
|
|
112
|
+
if (ownershipError) throw ownershipError;
|
|
113
|
+
const owner = await readOwner(path);
|
|
114
|
+
if (!owner || owner.token !== token) {
|
|
115
|
+
ownershipError = new Error("state lock ownership was lost; mutation aborted");
|
|
116
|
+
throw ownershipError;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
const renew = async () => {
|
|
120
|
+
await assertOwned();
|
|
121
|
+
const now = new Date();
|
|
122
|
+
await utimes(path, now, now);
|
|
123
|
+
};
|
|
124
|
+
const timer = setInterval(() => {
|
|
125
|
+
heartbeat = heartbeat.then(renew).catch((error) => { ownershipError ||= error; });
|
|
126
|
+
}, heartbeatIntervalMs);
|
|
127
|
+
timer.unref?.();
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
const result = await task({ token, acquiredAt, assertOwned });
|
|
131
|
+
await assertOwned();
|
|
132
|
+
return result;
|
|
133
|
+
} finally {
|
|
134
|
+
clearInterval(timer);
|
|
135
|
+
await heartbeat;
|
|
136
|
+
const owner = await readOwner(path).catch(() => null);
|
|
137
|
+
if (owner?.token === token) {
|
|
138
|
+
await unlink(path).catch((error) => {
|
|
139
|
+
if (error.code !== "ENOENT") throw error;
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -1,109 +1,109 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { realpathSync } from "node:fs";
|
|
3
|
-
import { homedir } from "node:os";
|
|
4
|
-
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { lstat, mkdir, realpath } from "node:fs/promises";
|
|
6
|
-
|
|
7
|
-
export async function canonicalPath(input = process.cwd()) {
|
|
8
|
-
return realpath(resolve(input));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function findProjectRoot(input = process.cwd()) {
|
|
12
|
-
if (process.env.AGENTSPINE_ROOT) return canonicalPath(process.env.AGENTSPINE_ROOT);
|
|
13
|
-
let cursor = await canonicalPath(input);
|
|
14
|
-
while (true) {
|
|
15
|
-
try {
|
|
16
|
-
await lstat(join(cursor, ".git"));
|
|
17
|
-
return cursor;
|
|
18
|
-
} catch (error) {
|
|
19
|
-
if (error.code !== "ENOENT") throw error;
|
|
20
|
-
}
|
|
21
|
-
const parent = dirname(cursor);
|
|
22
|
-
if (parent === cursor) return canonicalPath(input);
|
|
23
|
-
cursor = parent;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function isInside(parent, child, pathApi = { relative, isAbsolute, sep }) {
|
|
28
|
-
const value = pathApi.relative(parent, child);
|
|
29
|
-
return value === "" || (!pathApi.isAbsolute(value) && !value.startsWith(`..${pathApi.sep}`) && value !== "..");
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function ancestorsBetween(root, cwd) {
|
|
33
|
-
const result = [];
|
|
34
|
-
let cursor = resolve(cwd);
|
|
35
|
-
while (isInside(root, cursor)) {
|
|
36
|
-
result.push(cursor);
|
|
37
|
-
if (cursor === root) break;
|
|
38
|
-
const next = dirname(cursor);
|
|
39
|
-
if (next === cursor) break;
|
|
40
|
-
cursor = next;
|
|
41
|
-
}
|
|
42
|
-
return result.reverse();
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function stateRoot(env = process.env) {
|
|
46
|
-
if (env.AGENTSPINE_STATE_DIR) return resolve(env.AGENTSPINE_STATE_DIR);
|
|
47
|
-
if (process.platform === "win32") {
|
|
48
|
-
return join(env.LOCALAPPDATA || env.APPDATA || homedir(), "AgentSpine");
|
|
49
|
-
}
|
|
50
|
-
return join(env.XDG_STATE_HOME || join(homedir(), ".local", "state"), "agentspine");
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function comparablePath(value) {
|
|
54
|
-
let cursor = resolve(value);
|
|
55
|
-
const missing = [];
|
|
56
|
-
let canonical = cursor;
|
|
57
|
-
while (true) {
|
|
58
|
-
try {
|
|
59
|
-
canonical = realpathSync.native(cursor);
|
|
60
|
-
break;
|
|
61
|
-
} catch (error) {
|
|
62
|
-
if (error.code !== "ENOENT") break;
|
|
63
|
-
const parent = dirname(cursor);
|
|
64
|
-
if (parent === cursor) break;
|
|
65
|
-
missing.unshift(basename(cursor));
|
|
66
|
-
cursor = parent;
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
canonical = join(canonical, ...missing);
|
|
70
|
-
return canonical.replace(/[\\/]+$/, "");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function samePath(left, right) {
|
|
74
|
-
const normalize = comparablePath;
|
|
75
|
-
const a = normalize(left);
|
|
76
|
-
const b = normalize(right);
|
|
77
|
-
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export function isUserHomeRoot(root, env = process.env) {
|
|
81
|
-
const candidates = [homedir(), env.HOME, env.USERPROFILE,
|
|
82
|
-
env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : null]
|
|
83
|
-
.filter((value) => typeof value === "string" && value && isAbsolute(value));
|
|
84
|
-
return candidates.some((candidate) => samePath(root, candidate));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export function statePathIsScanExcluded(catalog, path, env = process.env) {
|
|
88
|
-
if (!catalog || catalog.schema !== "agentspine.catalog/v1" || typeof path !== "string") return false;
|
|
89
|
-
const localStateRoot = stateRoot(env);
|
|
90
|
-
const pathBelongsToState = isInside(localStateRoot, path);
|
|
91
|
-
const canonicalRoot = comparablePath(catalog.root);
|
|
92
|
-
const canonicalStateRoot = comparablePath(localStateRoot);
|
|
93
|
-
if (!pathBelongsToState && !isInside(canonicalRoot, comparablePath(path))) return true;
|
|
94
|
-
if (pathBelongsToState && !isInside(canonicalRoot, canonicalStateRoot)) return true;
|
|
95
|
-
return catalog.scanPolicy?.stateRoot === "excluded"
|
|
96
|
-
&& isUserHomeRoot(canonicalRoot, env)
|
|
97
|
-
&& isInside(canonicalRoot, canonicalStateRoot)
|
|
98
|
-
&& pathBelongsToState;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function projectId(root) {
|
|
102
|
-
return createHash("sha256").update(root).digest("hex").slice(0, 20);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export async function projectStateDir(root) {
|
|
106
|
-
const target = join(stateRoot(), "projects", projectId(root));
|
|
107
|
-
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
108
|
-
return target;
|
|
109
|
-
}
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { realpathSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { lstat, mkdir, realpath } from "node:fs/promises";
|
|
6
|
+
|
|
7
|
+
export async function canonicalPath(input = process.cwd()) {
|
|
8
|
+
return realpath(resolve(input));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function findProjectRoot(input = process.cwd()) {
|
|
12
|
+
if (process.env.AGENTSPINE_ROOT) return canonicalPath(process.env.AGENTSPINE_ROOT);
|
|
13
|
+
let cursor = await canonicalPath(input);
|
|
14
|
+
while (true) {
|
|
15
|
+
try {
|
|
16
|
+
await lstat(join(cursor, ".git"));
|
|
17
|
+
return cursor;
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.code !== "ENOENT") throw error;
|
|
20
|
+
}
|
|
21
|
+
const parent = dirname(cursor);
|
|
22
|
+
if (parent === cursor) return canonicalPath(input);
|
|
23
|
+
cursor = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isInside(parent, child, pathApi = { relative, isAbsolute, sep }) {
|
|
28
|
+
const value = pathApi.relative(parent, child);
|
|
29
|
+
return value === "" || (!pathApi.isAbsolute(value) && !value.startsWith(`..${pathApi.sep}`) && value !== "..");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ancestorsBetween(root, cwd) {
|
|
33
|
+
const result = [];
|
|
34
|
+
let cursor = resolve(cwd);
|
|
35
|
+
while (isInside(root, cursor)) {
|
|
36
|
+
result.push(cursor);
|
|
37
|
+
if (cursor === root) break;
|
|
38
|
+
const next = dirname(cursor);
|
|
39
|
+
if (next === cursor) break;
|
|
40
|
+
cursor = next;
|
|
41
|
+
}
|
|
42
|
+
return result.reverse();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function stateRoot(env = process.env) {
|
|
46
|
+
if (env.AGENTSPINE_STATE_DIR) return resolve(env.AGENTSPINE_STATE_DIR);
|
|
47
|
+
if (process.platform === "win32") {
|
|
48
|
+
return join(env.LOCALAPPDATA || env.APPDATA || homedir(), "AgentSpine");
|
|
49
|
+
}
|
|
50
|
+
return join(env.XDG_STATE_HOME || join(homedir(), ".local", "state"), "agentspine");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function comparablePath(value) {
|
|
54
|
+
let cursor = resolve(value);
|
|
55
|
+
const missing = [];
|
|
56
|
+
let canonical = cursor;
|
|
57
|
+
while (true) {
|
|
58
|
+
try {
|
|
59
|
+
canonical = realpathSync.native(cursor);
|
|
60
|
+
break;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
if (error.code !== "ENOENT") break;
|
|
63
|
+
const parent = dirname(cursor);
|
|
64
|
+
if (parent === cursor) break;
|
|
65
|
+
missing.unshift(basename(cursor));
|
|
66
|
+
cursor = parent;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
canonical = join(canonical, ...missing);
|
|
70
|
+
return canonical.replace(/[\\/]+$/, "");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function samePath(left, right) {
|
|
74
|
+
const normalize = comparablePath;
|
|
75
|
+
const a = normalize(left);
|
|
76
|
+
const b = normalize(right);
|
|
77
|
+
return process.platform === "win32" ? a.toLowerCase() === b.toLowerCase() : a === b;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function isUserHomeRoot(root, env = process.env) {
|
|
81
|
+
const candidates = [homedir(), env.HOME, env.USERPROFILE,
|
|
82
|
+
env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : null]
|
|
83
|
+
.filter((value) => typeof value === "string" && value && isAbsolute(value));
|
|
84
|
+
return candidates.some((candidate) => samePath(root, candidate));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function statePathIsScanExcluded(catalog, path, env = process.env) {
|
|
88
|
+
if (!catalog || catalog.schema !== "agentspine.catalog/v1" || typeof path !== "string") return false;
|
|
89
|
+
const localStateRoot = stateRoot(env);
|
|
90
|
+
const pathBelongsToState = isInside(localStateRoot, path);
|
|
91
|
+
const canonicalRoot = comparablePath(catalog.root);
|
|
92
|
+
const canonicalStateRoot = comparablePath(localStateRoot);
|
|
93
|
+
if (!pathBelongsToState && !isInside(canonicalRoot, comparablePath(path))) return true;
|
|
94
|
+
if (pathBelongsToState && !isInside(canonicalRoot, canonicalStateRoot)) return true;
|
|
95
|
+
return catalog.scanPolicy?.stateRoot === "excluded"
|
|
96
|
+
&& isUserHomeRoot(canonicalRoot, env)
|
|
97
|
+
&& isInside(canonicalRoot, canonicalStateRoot)
|
|
98
|
+
&& pathBelongsToState;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function projectId(root) {
|
|
102
|
+
return createHash("sha256").update(root).digest("hex").slice(0, 20);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function projectStateDir(root) {
|
|
106
|
+
const target = join(stateRoot(), "projects", projectId(root));
|
|
107
|
+
await mkdir(target, { recursive: true, mode: 0o700 });
|
|
108
|
+
return target;
|
|
109
|
+
}
|