@visorcraft/idlehands 1.3.4 → 1.3.5
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/dist/agent/formatting.js +1 -5
- package/dist/agent/formatting.js.map +1 -1
- package/dist/agent.js +114 -26
- package/dist/agent.js.map +1 -1
- package/dist/anton/reporter.js +2 -20
- package/dist/anton/reporter.js.map +1 -1
- package/dist/bot/commands.js +24 -0
- package/dist/bot/commands.js.map +1 -1
- package/dist/bot/discord-commands.js +833 -0
- package/dist/bot/discord-commands.js.map +1 -0
- package/dist/bot/discord-routing.js +1 -8
- package/dist/bot/discord-routing.js.map +1 -1
- package/dist/bot/discord.js +15 -813
- package/dist/bot/discord.js.map +1 -1
- package/dist/bot/session-manager.js +52 -0
- package/dist/bot/session-manager.js.map +1 -1
- package/dist/bot/telegram-commands.js +201 -0
- package/dist/bot/telegram-commands.js.map +1 -0
- package/dist/bot/telegram.js +10 -309
- package/dist/bot/telegram.js.map +1 -1
- package/dist/cli/commands/project.js +52 -0
- package/dist/cli/commands/project.js.map +1 -1
- package/dist/context.js +1 -3
- package/dist/context.js.map +1 -1
- package/dist/progress/ir.js +0 -3
- package/dist/progress/ir.js.map +1 -1
- package/dist/progress/tool-summary.js +1 -4
- package/dist/progress/tool-summary.js.map +1 -1
- package/dist/progress/turn-progress.js +1 -5
- package/dist/progress/turn-progress.js.map +1 -1
- package/dist/runtime/executor.js +1 -3
- package/dist/runtime/executor.js.map +1 -1
- package/dist/runtime/health.js +2 -1
- package/dist/runtime/health.js.map +1 -1
- package/dist/shared/async.js +5 -0
- package/dist/shared/async.js.map +1 -0
- package/dist/shared/config-utils.js +8 -0
- package/dist/shared/config-utils.js.map +1 -0
- package/dist/shared/format.js +19 -0
- package/dist/shared/format.js.map +1 -0
- package/dist/shared/math.js +5 -0
- package/dist/shared/math.js.map +1 -0
- package/dist/shared/strings.js +8 -0
- package/dist/shared/strings.js.map +1 -0
- package/dist/tools/patch.js +82 -0
- package/dist/tools/patch.js.map +1 -0
- package/dist/tools/path-safety.js +89 -0
- package/dist/tools/path-safety.js.map +1 -0
- package/dist/tools/undo.js +141 -0
- package/dist/tools/undo.js.map +1 -0
- package/dist/tools.js +11 -289
- package/dist/tools.js.map +1 -1
- package/dist/tui/event-bridge.js +1 -3
- package/dist/tui/event-bridge.js.map +1 -1
- package/dist/tui/render.js +1 -5
- package/dist/tui/render.js.map +1 -1
- package/dist/vault.js +1 -5
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"path-safety.js","sourceRoot":"","sources":["../../src/tools/path-safety.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Backup system and undo functionality for file tools.
|
|
3
|
+
* Manages file backups with FIFO rotation and provides undo support.
|
|
4
|
+
*/
|
|
5
|
+
import fs from 'node:fs/promises';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import crypto from 'node:crypto';
|
|
8
|
+
import { stateDir } from '../utils.js';
|
|
9
|
+
import { resolvePath, redactPath } from './path-safety.js';
|
|
10
|
+
const DEFAULT_MAX_BACKUPS_PER_FILE = 5;
|
|
11
|
+
function defaultBackupDir() {
|
|
12
|
+
return path.join(stateDir(), 'backups');
|
|
13
|
+
}
|
|
14
|
+
function sha256(s) {
|
|
15
|
+
return crypto.createHash('sha256').update(s).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
function keyFromPath(absPath) {
|
|
18
|
+
return sha256(absPath);
|
|
19
|
+
}
|
|
20
|
+
function backupDirForPath(ctx, absPath) {
|
|
21
|
+
const bdir = ctx.backupDir ?? defaultBackupDir();
|
|
22
|
+
const key = keyFromPath(absPath);
|
|
23
|
+
return { bdir, key, keyDir: path.join(bdir, key) };
|
|
24
|
+
}
|
|
25
|
+
function formatBackupTs() {
|
|
26
|
+
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
27
|
+
}
|
|
28
|
+
async function restoreLatestBackup(absPath, ctx) {
|
|
29
|
+
const { key, keyDir } = backupDirForPath(ctx, absPath);
|
|
30
|
+
const legacyDir = ctx.backupDir ?? defaultBackupDir();
|
|
31
|
+
const latestInDir = async (dir) => {
|
|
32
|
+
const ents = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
33
|
+
return ents
|
|
34
|
+
.filter((e) => e.isFile())
|
|
35
|
+
.map((e) => e.name)
|
|
36
|
+
.filter((n) => n.endsWith('.bak'))
|
|
37
|
+
.sort()
|
|
38
|
+
.reverse()[0];
|
|
39
|
+
};
|
|
40
|
+
let bakDir = keyDir;
|
|
41
|
+
let bakFile = await latestInDir(keyDir);
|
|
42
|
+
if (!bakFile) {
|
|
43
|
+
// Compatibility with older flat backup format (without nested key dir).
|
|
44
|
+
const ents = await fs.readdir(legacyDir, { withFileTypes: true }).catch(() => []);
|
|
45
|
+
const legacy = ents
|
|
46
|
+
.filter((e) => e.isFile())
|
|
47
|
+
.map((e) => e.name)
|
|
48
|
+
.filter((n) => n.startsWith(`${key}.`) && !n.endsWith('.json'))
|
|
49
|
+
.sort()
|
|
50
|
+
.reverse()[0];
|
|
51
|
+
if (legacy) {
|
|
52
|
+
bakFile = legacy;
|
|
53
|
+
bakDir = legacyDir;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!bakFile) {
|
|
57
|
+
throw new Error(`undo: no backups found for ${absPath} in ${legacyDir}`);
|
|
58
|
+
}
|
|
59
|
+
const bakPath = path.join(bakDir, bakFile);
|
|
60
|
+
const buf = await fs.readFile(bakPath);
|
|
61
|
+
// backup current file before restoring
|
|
62
|
+
await backupFile(absPath, ctx);
|
|
63
|
+
await atomicWrite(absPath, buf);
|
|
64
|
+
return `restored ${absPath} from backup ${bakPath}`;
|
|
65
|
+
}
|
|
66
|
+
export async function rotateBackups(absPath, ctx) {
|
|
67
|
+
const { keyDir } = backupDirForPath(ctx, absPath);
|
|
68
|
+
const limit = ctx.maxBackupsPerFile ?? DEFAULT_MAX_BACKUPS_PER_FILE;
|
|
69
|
+
if (limit <= 0)
|
|
70
|
+
return;
|
|
71
|
+
await fs.mkdir(keyDir, { recursive: true });
|
|
72
|
+
const ents = await fs.readdir(keyDir, { withFileTypes: true }).catch(() => []);
|
|
73
|
+
const backups = ents
|
|
74
|
+
.filter((e) => e.isFile())
|
|
75
|
+
.map((e) => e.name)
|
|
76
|
+
.filter((n) => n.endsWith('.bak'))
|
|
77
|
+
.sort(); // oldest → newest due to ISO timestamp
|
|
78
|
+
const toDelete = backups.length > limit ? backups.slice(0, backups.length - limit) : [];
|
|
79
|
+
for (const name of toDelete) {
|
|
80
|
+
const bak = path.join(keyDir, name);
|
|
81
|
+
const meta = path.join(keyDir, `${name.replace(/\.bak$/, '')}.meta.json`);
|
|
82
|
+
await fs.rm(bak, { force: true }).catch(() => { });
|
|
83
|
+
await fs.rm(meta, { force: true }).catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function backupFile(absPath, ctx) {
|
|
87
|
+
const { bdir, keyDir } = backupDirForPath(ctx, absPath);
|
|
88
|
+
await fs.mkdir(bdir, { recursive: true });
|
|
89
|
+
await fs.mkdir(keyDir, { recursive: true });
|
|
90
|
+
// Auto-create .gitignore in state dir to prevent backups from being committed
|
|
91
|
+
const gitignorePath = path.join(bdir, '.gitignore');
|
|
92
|
+
await fs.writeFile(gitignorePath, '*\n', { flag: 'wx' }).catch(() => { });
|
|
93
|
+
// 'wx' flag = create only if doesn't exist, silently skip if it does
|
|
94
|
+
const st = await fs.stat(absPath).catch(() => null);
|
|
95
|
+
if (!st || !st.isFile())
|
|
96
|
+
return;
|
|
97
|
+
const content = await fs.readFile(absPath);
|
|
98
|
+
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
99
|
+
const ts = formatBackupTs();
|
|
100
|
+
const bakName = `${ts}.bak`;
|
|
101
|
+
const metaName = `${ts}.meta.json`;
|
|
102
|
+
const bakPath = path.join(keyDir, bakName);
|
|
103
|
+
const metaPath = path.join(keyDir, metaName);
|
|
104
|
+
await fs.writeFile(bakPath, content);
|
|
105
|
+
await fs.writeFile(metaPath, JSON.stringify({ original_path: absPath, timestamp: ts, size: st.size, sha256_before: hash }, null, 2) + '\n', 'utf8');
|
|
106
|
+
await rotateBackups(absPath, ctx);
|
|
107
|
+
}
|
|
108
|
+
export async function atomicWrite(absPath, data) {
|
|
109
|
+
const dir = path.dirname(absPath);
|
|
110
|
+
await fs.mkdir(dir, { recursive: true });
|
|
111
|
+
// Capture original permissions before overwriting
|
|
112
|
+
const origStat = await fs.stat(absPath).catch(() => null);
|
|
113
|
+
const origMode = origStat?.mode;
|
|
114
|
+
const tmp = path.join(dir, `.${path.basename(absPath)}.idlehands.tmp.${process.pid}.${Date.now()}`);
|
|
115
|
+
await fs.writeFile(tmp, data);
|
|
116
|
+
// Restore original file mode bits if the file existed
|
|
117
|
+
if (origMode != null) {
|
|
118
|
+
await fs.chmod(tmp, origMode & 0o7777).catch(() => { });
|
|
119
|
+
}
|
|
120
|
+
await fs.rename(tmp, absPath);
|
|
121
|
+
}
|
|
122
|
+
export async function undo_path(ctx, args) {
|
|
123
|
+
const directPath = args?.path === undefined ? undefined : String(args.path);
|
|
124
|
+
const p = directPath ? resolvePath(ctx, directPath) : ctx.lastEditedPath;
|
|
125
|
+
if (!p)
|
|
126
|
+
throw new Error('undo: missing path');
|
|
127
|
+
const absCwd = path.resolve(ctx.cwd);
|
|
128
|
+
const redactedPath = redactPath(p, absCwd);
|
|
129
|
+
if (!ctx.noConfirm && ctx.confirm) {
|
|
130
|
+
const ok = await ctx.confirm(`Restore latest backup for:\n ${redactedPath}\nThis will overwrite the current file. Proceed? (y/N) `);
|
|
131
|
+
if (!ok)
|
|
132
|
+
return 'undo: cancelled';
|
|
133
|
+
}
|
|
134
|
+
if (!ctx.noConfirm && !ctx.confirm) {
|
|
135
|
+
throw new Error('undo: confirmation required (run with --no-confirm/--yolo or in interactive mode)');
|
|
136
|
+
}
|
|
137
|
+
if (ctx.dryRun)
|
|
138
|
+
return `dry-run: would restore latest backup for ${redactedPath}`;
|
|
139
|
+
return await restoreLatestBackup(p, ctx);
|
|
140
|
+
}
|
|
141
|
+
//# sourceMappingURL=undo.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"undo.js","sourceRoot":"","sources":["../../src/tools/undo.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAClC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AAGjC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACvC,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAE3D,MAAM,4BAA4B,GAAG,CAAC,CAAC;AAEvC,SAAS,gBAAgB;IACvB,OAAO,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,EAAE,SAAS,CAAC,CAAC;AAC1C,CAAC;AAED,SAAS,MAAM,CAAC,CAAS;IACvB,OAAO,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AAC7D,CAAC;AAED,SAAS,WAAW,CAAC,OAAe;IAClC,OAAO,MAAM,CAAC,OAAO,CAAC,CAAC;AACzB,CAAC;AAED,SAAS,gBAAgB,CAAC,GAAgB,EAAE,OAAe;IACzD,MAAM,IAAI,GAAG,GAAG,CAAC,SAAS,IAAI,gBAAgB,EAAE,CAAC;IACjD,MAAM,GAAG,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,cAAc;IACrB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AACxD,CAAC;AAED,KAAK,UAAU,mBAAmB,CAAC,OAAe,EAAE,GAAgB;IAClE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,GAAG,CAAC,SAAS,IAAI,gBAAgB,EAAE,CAAC;IAEtD,MAAM,WAAW,GAAG,KAAK,EAAE,GAAW,EAA+B,EAAE;QACrE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAC5E,OAAO,IAAI;aACR,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;aACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;aACjC,IAAI,EAAE;aACN,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC,CAAC;IAEF,IAAI,MAAM,GAAG,MAAM,CAAC;IACpB,IAAI,OAAO,GAAG,MAAM,WAAW,CAAC,MAAM,CAAC,CAAC;IAExC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,wEAAwE;QACxE,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;QAClF,MAAM,MAAM,GAAG,IAAI;aAChB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;aACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;aAClB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;aAC9D,IAAI,EAAE;aACN,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QAChB,IAAI,MAAM,EAAE,CAAC;YACX,OAAO,GAAG,MAAM,CAAC;YACjB,MAAM,GAAG,SAAS,CAAC;QACrB,CAAC;IACH,CAAC;IAED,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,KAAK,CAAC,8BAA8B,OAAO,OAAO,SAAS,EAAE,CAAC,CAAC;IAC3E,CAAC;IAED,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAEvC,uCAAuC;IACvC,MAAM,UAAU,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAC/B,MAAM,WAAW,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IAChC,OAAO,YAAY,OAAO,gBAAgB,OAAO,EAAE,CAAC;AACtD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,OAAe,EAAE,GAAgB;IACnE,MAAM,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAClD,MAAM,KAAK,GAAG,GAAG,CAAC,iBAAiB,IAAI,4BAA4B,CAAC;IACpE,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO;IAEvB,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,MAAM,IAAI,GAAG,MAAM,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IAC/E,MAAM,OAAO,GAAG,IAAI;SACjB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC;SACzB,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;SACjC,IAAI,EAAE,CAAC,CAAC,uCAAuC;IAElD,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,MAAM,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACxF,KAAK,MAAM,IAAI,IAAI,QAAQ,EAAE,CAAC;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;QACpC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC;QAC1E,MAAM,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;QACnD,MAAM,EAAE,CAAC,EAAE,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IACtD,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,OAAe,EAAE,GAAgB;IAChE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,gBAAgB,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IACxD,MAAM,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1C,MAAM,EAAE,CAAC,KAAK,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,8EAA8E;IAC9E,MAAM,aAAa,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;IACpD,MAAM,EAAE,CAAC,SAAS,CAAC,aAAa,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1E,qEAAqE;IAErE,MAAM,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IACpD,IAAI,CAAC,EAAE,IAAI,CAAC,EAAE,CAAC,MAAM,EAAE;QAAE,OAAO;IAEhC,MAAM,OAAO,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,MAAM,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IAEvE,MAAM,EAAE,GAAG,cAAc,EAAE,CAAC;IAC5B,MAAM,OAAO,GAAG,GAAG,EAAE,MAAM,CAAC;IAC5B,MAAM,QAAQ,GAAG,GAAG,EAAE,YAAY,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;IAE7C,MAAM,EAAE,CAAC,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACrC,MAAM,EAAE,CAAC,SAAS,CAChB,QAAQ,EACR,IAAI,CAAC,SAAS,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,EAC7G,MAAM,CACP,CAAC;IAEF,MAAM,aAAa,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,OAAe,EAAE,IAAqB;IACtE,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAEzC,kDAAkD;IAClD,MAAM,QAAQ,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAC1D,MAAM,QAAQ,GAAG,QAAQ,EAAE,IAAI,CAAC;IAEhC,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,kBAAkB,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACpG,MAAM,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAE9B,sDAAsD;IACtD,IAAI,QAAQ,IAAI,IAAI,EAAE,CAAC;QACrB,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,QAAQ,GAAG,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED,MAAM,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;AAChC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAAC,GAAgB,EAAE,IAAS;IACzD,MAAM,UAAU,GAAG,IAAI,EAAE,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC5E,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,cAAc,CAAC;IACzE,IAAI,CAAC,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,oBAAoB,CAAC,CAAC;IAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IACrC,MAAM,YAAY,GAAG,UAAU,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;IAC3C,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,GAAG,CAAC,OAAO,EAAE,CAAC;QAClC,MAAM,EAAE,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,iCAAiC,YAAY,yDAAyD,CAAC,CAAC;QACrI,IAAI,CAAC,EAAE;YAAE,OAAO,iBAAiB,CAAC;IACpC,CAAC;IACD,IAAI,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CAAC,mFAAmF,CAAC,CAAC;IACvG,CAAC;IACD,IAAI,GAAG,CAAC,MAAM;QAAE,OAAO,4CAA4C,YAAY,EAAE,CAAC;IAClF,OAAO,MAAM,mBAAmB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC3C,CAAC"}
|
package/dist/tools.js
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import fs from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import crypto from 'node:crypto';
|
|
4
3
|
import { spawn, spawnSync } from 'node:child_process';
|
|
5
4
|
import { ToolError } from './tools/tool-error.js';
|
|
6
5
|
import { checkExecSafety, checkPathSafety, isProtectedDeleteTarget } from './safety.js';
|
|
7
6
|
import { sys_context as sysContextTool } from './sys/context.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
7
|
+
import { isWithinDir, resolvePath, redactPath, checkCwdWarning, enforceMutationWithinCwd, } from './tools/path-safety.js';
|
|
8
|
+
import { normalizePatchPath, extractTouchedFilesFromPatch, } from './tools/patch.js';
|
|
9
|
+
import { atomicWrite, backupFile } from './tools/undo.js';
|
|
10
|
+
import { shellEscape, BASH_PATH } from './utils.js';
|
|
11
|
+
// Re-export from extracted modules so existing imports don't break
|
|
12
|
+
export { atomicWrite, undo_path } from './tools/undo.js';
|
|
13
|
+
// Backup/undo system imported from tools/undo.ts (atomicWrite, backupFile, undo_path)
|
|
10
14
|
/**
|
|
11
15
|
* Build a read-back snippet showing the region around a mutation.
|
|
12
16
|
* Returns numbered lines ±contextLines around the changed area, capped to avoid bloat.
|
|
@@ -73,61 +77,6 @@ function guessMimeType(filePath, buf) {
|
|
|
73
77
|
};
|
|
74
78
|
return extMap[ext] ?? 'application/octet-stream';
|
|
75
79
|
}
|
|
76
|
-
function defaultBackupDir() {
|
|
77
|
-
return path.join(stateDir(), 'backups');
|
|
78
|
-
}
|
|
79
|
-
function sha256(s) {
|
|
80
|
-
return crypto.createHash('sha256').update(s).digest('hex');
|
|
81
|
-
}
|
|
82
|
-
function keyFromPath(absPath) {
|
|
83
|
-
return sha256(absPath);
|
|
84
|
-
}
|
|
85
|
-
function backupDirForPath(ctx, absPath) {
|
|
86
|
-
const bdir = ctx.backupDir ?? defaultBackupDir();
|
|
87
|
-
const key = keyFromPath(absPath);
|
|
88
|
-
return { bdir, key, keyDir: path.join(bdir, key) };
|
|
89
|
-
}
|
|
90
|
-
function formatBackupTs() {
|
|
91
|
-
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
92
|
-
}
|
|
93
|
-
async function restoreLatestBackup(absPath, ctx) {
|
|
94
|
-
const { key, keyDir } = backupDirForPath(ctx, absPath);
|
|
95
|
-
const legacyDir = ctx.backupDir ?? defaultBackupDir();
|
|
96
|
-
const latestInDir = async (dir) => {
|
|
97
|
-
const ents = await fs.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
98
|
-
return ents
|
|
99
|
-
.filter((e) => e.isFile())
|
|
100
|
-
.map((e) => e.name)
|
|
101
|
-
.filter((n) => n.endsWith('.bak'))
|
|
102
|
-
.sort()
|
|
103
|
-
.reverse()[0];
|
|
104
|
-
};
|
|
105
|
-
let bakDir = keyDir;
|
|
106
|
-
let bakFile = await latestInDir(keyDir);
|
|
107
|
-
if (!bakFile) {
|
|
108
|
-
// Compatibility with older flat backup format (without nested key dir).
|
|
109
|
-
const ents = await fs.readdir(legacyDir, { withFileTypes: true }).catch(() => []);
|
|
110
|
-
const legacy = ents
|
|
111
|
-
.filter((e) => e.isFile())
|
|
112
|
-
.map((e) => e.name)
|
|
113
|
-
.filter((n) => n.startsWith(`${key}.`) && !n.endsWith('.json'))
|
|
114
|
-
.sort()
|
|
115
|
-
.reverse()[0];
|
|
116
|
-
if (legacy) {
|
|
117
|
-
bakFile = legacy;
|
|
118
|
-
bakDir = legacyDir;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
if (!bakFile) {
|
|
122
|
-
throw new Error(`undo: no backups found for ${absPath} in ${legacyDir}`);
|
|
123
|
-
}
|
|
124
|
-
const bakPath = path.join(bakDir, bakFile);
|
|
125
|
-
const buf = await fs.readFile(bakPath);
|
|
126
|
-
// backup current file before restoring
|
|
127
|
-
await backupFile(absPath, ctx);
|
|
128
|
-
await atomicWrite(absPath, buf);
|
|
129
|
-
return `restored ${absPath} from backup ${bakPath}`;
|
|
130
|
-
}
|
|
131
80
|
function stripAnsi(s) {
|
|
132
81
|
// eslint-disable-next-line no-control-regex
|
|
133
82
|
return s
|
|
@@ -217,48 +166,6 @@ function truncateBytes(s, maxBytes, totalBytesHint) {
|
|
|
217
166
|
const cut = b.subarray(0, maxBytes);
|
|
218
167
|
return { text: cut.toString('utf8') + `\n[truncated, ${total} bytes total]`, truncated: true };
|
|
219
168
|
}
|
|
220
|
-
async function rotateBackups(absPath, ctx) {
|
|
221
|
-
const { keyDir } = backupDirForPath(ctx, absPath);
|
|
222
|
-
const limit = ctx.maxBackupsPerFile ?? DEFAULT_MAX_BACKUPS_PER_FILE;
|
|
223
|
-
if (limit <= 0)
|
|
224
|
-
return;
|
|
225
|
-
await fs.mkdir(keyDir, { recursive: true });
|
|
226
|
-
const ents = await fs.readdir(keyDir, { withFileTypes: true }).catch(() => []);
|
|
227
|
-
const backups = ents
|
|
228
|
-
.filter((e) => e.isFile())
|
|
229
|
-
.map((e) => e.name)
|
|
230
|
-
.filter((n) => n.endsWith('.bak'))
|
|
231
|
-
.sort(); // oldest → newest due to ISO timestamp
|
|
232
|
-
const toDelete = backups.length > limit ? backups.slice(0, backups.length - limit) : [];
|
|
233
|
-
for (const name of toDelete) {
|
|
234
|
-
const bak = path.join(keyDir, name);
|
|
235
|
-
const meta = path.join(keyDir, `${name.replace(/\.bak$/, '')}.meta.json`);
|
|
236
|
-
await fs.rm(bak, { force: true }).catch(() => { });
|
|
237
|
-
await fs.rm(meta, { force: true }).catch(() => { });
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
async function backupFile(absPath, ctx) {
|
|
241
|
-
const { bdir, keyDir } = backupDirForPath(ctx, absPath);
|
|
242
|
-
await fs.mkdir(bdir, { recursive: true });
|
|
243
|
-
await fs.mkdir(keyDir, { recursive: true });
|
|
244
|
-
// Auto-create .gitignore in state dir to prevent backups from being committed
|
|
245
|
-
const gitignorePath = path.join(bdir, '.gitignore');
|
|
246
|
-
await fs.writeFile(gitignorePath, '*\n', { flag: 'wx' }).catch(() => { });
|
|
247
|
-
// 'wx' flag = create only if doesn't exist, silently skip if it does
|
|
248
|
-
const st = await fs.stat(absPath).catch(() => null);
|
|
249
|
-
if (!st || !st.isFile())
|
|
250
|
-
return;
|
|
251
|
-
const content = await fs.readFile(absPath);
|
|
252
|
-
const hash = crypto.createHash('sha256').update(content).digest('hex');
|
|
253
|
-
const ts = formatBackupTs();
|
|
254
|
-
const bakName = `${ts}.bak`;
|
|
255
|
-
const metaName = `${ts}.meta.json`;
|
|
256
|
-
const bakPath = path.join(keyDir, bakName);
|
|
257
|
-
const metaPath = path.join(keyDir, metaName);
|
|
258
|
-
await fs.writeFile(bakPath, content);
|
|
259
|
-
await fs.writeFile(metaPath, JSON.stringify({ original_path: absPath, timestamp: ts, size: st.size, sha256_before: hash }, null, 2) + '\n', 'utf8');
|
|
260
|
-
await rotateBackups(absPath, ctx);
|
|
261
|
-
}
|
|
262
169
|
async function checkpointReplay(ctx, payload) {
|
|
263
170
|
if (!ctx.replay)
|
|
264
171
|
return '';
|
|
@@ -279,39 +186,6 @@ async function checkpointReplay(ctx, payload) {
|
|
|
279
186
|
return ` replay_skipped: ${e?.message ?? String(e)}`;
|
|
280
187
|
}
|
|
281
188
|
}
|
|
282
|
-
export async function atomicWrite(absPath, data) {
|
|
283
|
-
const dir = path.dirname(absPath);
|
|
284
|
-
await fs.mkdir(dir, { recursive: true });
|
|
285
|
-
// Capture original permissions before overwriting
|
|
286
|
-
const origStat = await fs.stat(absPath).catch(() => null);
|
|
287
|
-
const origMode = origStat?.mode;
|
|
288
|
-
const tmp = path.join(dir, `.${path.basename(absPath)}.idlehands.tmp.${process.pid}.${Date.now()}`);
|
|
289
|
-
await fs.writeFile(tmp, data);
|
|
290
|
-
// Restore original file mode bits if the file existed
|
|
291
|
-
if (origMode != null) {
|
|
292
|
-
await fs.chmod(tmp, origMode & 0o7777).catch(() => { });
|
|
293
|
-
}
|
|
294
|
-
await fs.rename(tmp, absPath);
|
|
295
|
-
}
|
|
296
|
-
export async function undo_path(ctx, args) {
|
|
297
|
-
const directPath = args?.path === undefined ? undefined : String(args.path);
|
|
298
|
-
const p = directPath ? resolvePath(ctx, directPath) : ctx.lastEditedPath;
|
|
299
|
-
if (!p)
|
|
300
|
-
throw new Error('undo: missing path');
|
|
301
|
-
const absCwd = path.resolve(ctx.cwd);
|
|
302
|
-
const redactedPath = redactPath(p, absCwd);
|
|
303
|
-
if (!ctx.noConfirm && ctx.confirm) {
|
|
304
|
-
const ok = await ctx.confirm(`Restore latest backup for:\n ${redactedPath}\nThis will overwrite the current file. Proceed? (y/N) `);
|
|
305
|
-
if (!ok)
|
|
306
|
-
return 'undo: cancelled';
|
|
307
|
-
}
|
|
308
|
-
if (!ctx.noConfirm && !ctx.confirm) {
|
|
309
|
-
throw new Error('undo: confirmation required (run with --no-confirm/--yolo or in interactive mode)');
|
|
310
|
-
}
|
|
311
|
-
if (ctx.dryRun)
|
|
312
|
-
return `dry-run: would restore latest backup for ${redactedPath}`;
|
|
313
|
-
return await restoreLatestBackup(p, ctx);
|
|
314
|
-
}
|
|
315
189
|
export async function read_file(ctx, args) {
|
|
316
190
|
const p = resolvePath(ctx, args?.path);
|
|
317
191
|
const absCwd = path.resolve(ctx.cwd);
|
|
@@ -686,82 +560,8 @@ export async function edit_file(ctx, args) {
|
|
|
686
560
|
const readback = mutationReadback(next, editStartLine, editEndLine);
|
|
687
561
|
return `edited ${redactedPath} (replace_all=${replaceAll})${replayNote}${cwdWarning}${readback}`;
|
|
688
562
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
if (!s || s === '/dev/null')
|
|
692
|
-
return '';
|
|
693
|
-
// Strip quotes some generators add
|
|
694
|
-
s = s.replace(/^"|"$/g, '');
|
|
695
|
-
// Drop common diff prefixes
|
|
696
|
-
s = s.replace(/^[ab]\//, '').replace(/^\.\/+/, '');
|
|
697
|
-
// Normalize to posix separators for diffs
|
|
698
|
-
s = s.replace(/\\/g, '/');
|
|
699
|
-
const norm = path.posix.normalize(s);
|
|
700
|
-
if (norm.startsWith('../') || norm === '..' || norm.startsWith('/')) {
|
|
701
|
-
throw new Error(`apply_patch: unsafe path in patch: ${JSON.stringify(s)}`);
|
|
702
|
-
}
|
|
703
|
-
return norm;
|
|
704
|
-
}
|
|
705
|
-
function extractTouchedFilesFromPatch(patchText) {
|
|
706
|
-
const paths = [];
|
|
707
|
-
const created = new Set();
|
|
708
|
-
const deleted = new Set();
|
|
709
|
-
let pendingOld = null;
|
|
710
|
-
let pendingNew = null;
|
|
711
|
-
const seen = new Set();
|
|
712
|
-
const lines = String(patchText ?? '').split(/\r?\n/);
|
|
713
|
-
for (const line of lines) {
|
|
714
|
-
// Primary: git-style header
|
|
715
|
-
if (line.startsWith('diff --git ')) {
|
|
716
|
-
const m = /^diff --git\s+a\/(.+?)\s+b\/(.+?)\s*$/.exec(line);
|
|
717
|
-
if (m) {
|
|
718
|
-
const aPath = normalizePatchPath(m[1]);
|
|
719
|
-
const bPath = normalizePatchPath(m[2]);
|
|
720
|
-
const use = bPath || aPath;
|
|
721
|
-
if (use && !seen.has(use)) {
|
|
722
|
-
seen.add(use);
|
|
723
|
-
paths.push(use);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
pendingOld = null;
|
|
727
|
-
pendingNew = null;
|
|
728
|
-
continue;
|
|
729
|
-
}
|
|
730
|
-
// Fallback: unified diff headers
|
|
731
|
-
if (line.startsWith('--- ')) {
|
|
732
|
-
pendingOld = line.slice(4).trim();
|
|
733
|
-
continue;
|
|
734
|
-
}
|
|
735
|
-
if (line.startsWith('+++ ')) {
|
|
736
|
-
pendingNew = line.slice(4).trim();
|
|
737
|
-
const oldP = pendingOld ? pendingOld.replace(/^a\//, '').trim() : '';
|
|
738
|
-
const newP = pendingNew ? pendingNew.replace(/^b\//, '').trim() : '';
|
|
739
|
-
const oldIsDevNull = oldP === '/dev/null';
|
|
740
|
-
const newIsDevNull = newP === '/dev/null';
|
|
741
|
-
if (!newIsDevNull) {
|
|
742
|
-
const rel = normalizePatchPath(newP);
|
|
743
|
-
if (rel && !seen.has(rel)) {
|
|
744
|
-
seen.add(rel);
|
|
745
|
-
paths.push(rel);
|
|
746
|
-
}
|
|
747
|
-
if (oldIsDevNull)
|
|
748
|
-
created.add(rel);
|
|
749
|
-
}
|
|
750
|
-
if (!oldIsDevNull && newIsDevNull) {
|
|
751
|
-
const rel = normalizePatchPath(oldP);
|
|
752
|
-
if (rel && !seen.has(rel)) {
|
|
753
|
-
seen.add(rel);
|
|
754
|
-
paths.push(rel);
|
|
755
|
-
}
|
|
756
|
-
deleted.add(rel);
|
|
757
|
-
}
|
|
758
|
-
pendingOld = null;
|
|
759
|
-
pendingNew = null;
|
|
760
|
-
continue;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
return { paths, created, deleted };
|
|
764
|
-
}
|
|
563
|
+
// Patch parsing helpers imported from tools/patch.ts:
|
|
564
|
+
// PatchTouchInfo, normalizePatchPath, extractTouchedFilesFromPatch
|
|
765
565
|
async function runCommandWithStdin(cmd, cmdArgs, stdinText, cwd, maxOutBytes) {
|
|
766
566
|
return await new Promise((resolve, reject) => {
|
|
767
567
|
const child = spawn(cmd, cmdArgs, { cwd, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
@@ -1605,86 +1405,8 @@ export async function vault_search(ctx, args) {
|
|
|
1605
1405
|
export async function sys_context(ctx, args) {
|
|
1606
1406
|
return sysContextTool(ctx, args);
|
|
1607
1407
|
}
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
* Handles the classic root directory edge case: when dir is `/`, every absolute path is valid.
|
|
1611
|
-
*/
|
|
1612
|
-
function isWithinDir(target, dir) {
|
|
1613
|
-
if (dir === '/')
|
|
1614
|
-
return target.startsWith('/') && !target.includes('..');
|
|
1615
|
-
const rel = path.relative(dir, target);
|
|
1616
|
-
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
1617
|
-
}
|
|
1618
|
-
function resolvePath(ctx, p) {
|
|
1619
|
-
if (typeof p !== 'string' || !p.trim())
|
|
1620
|
-
throw new Error('missing path');
|
|
1621
|
-
return path.resolve(ctx.cwd, p);
|
|
1622
|
-
}
|
|
1623
|
-
/**
|
|
1624
|
-
* Redact a path for safe output.
|
|
1625
|
-
* - Paths within cwd are shown as relative paths
|
|
1626
|
-
* - Paths outside cwd are redacted as [outside-cwd]/basename
|
|
1627
|
-
*/
|
|
1628
|
-
function redactPath(filePath, absCwd) {
|
|
1629
|
-
const resolved = path.resolve(filePath);
|
|
1630
|
-
if (isWithinDir(resolved, absCwd)) {
|
|
1631
|
-
return path.relative(absCwd, resolved);
|
|
1632
|
-
}
|
|
1633
|
-
const basename = path.basename(resolved);
|
|
1634
|
-
return `[outside-cwd]/${basename}`;
|
|
1635
|
-
}
|
|
1636
|
-
/**
|
|
1637
|
-
* Check if a resolved path is outside the working directory.
|
|
1638
|
-
* Returns a model-visible warning string if so, empty string otherwise.
|
|
1639
|
-
*/
|
|
1640
|
-
function checkCwdWarning(tool, resolvedPath, ctx) {
|
|
1641
|
-
const absCwd = path.resolve(ctx.cwd);
|
|
1642
|
-
if (isWithinDir(resolvedPath, absCwd))
|
|
1643
|
-
return '';
|
|
1644
|
-
const warning = `\n[WARNING] Path "${resolvedPath}" is OUTSIDE the working directory "${absCwd}". You MUST use relative paths and work within the project directory. Do NOT create or edit files outside the cwd.`;
|
|
1645
|
-
console.warn(`[warning] ${tool}: path "${resolvedPath}" is outside the working directory "${absCwd}".`);
|
|
1646
|
-
return warning;
|
|
1647
|
-
}
|
|
1648
|
-
/**
|
|
1649
|
-
* Hard guard for mutating file tools in normal code mode.
|
|
1650
|
-
* In code mode, writing outside cwd is always blocked to prevent accidental edits
|
|
1651
|
-
* in the wrong repository. System mode keeps broader path freedom for /etc workflows.
|
|
1652
|
-
*/
|
|
1653
|
-
function enforceMutationWithinCwd(tool, resolvedPath, ctx) {
|
|
1654
|
-
if (ctx.mode === 'sys')
|
|
1655
|
-
return;
|
|
1656
|
-
const absTarget = path.resolve(resolvedPath);
|
|
1657
|
-
const absCwd = path.resolve(ctx.cwd);
|
|
1658
|
-
const roots = (ctx.allowedWriteRoots ?? []).map((r) => path.resolve(r));
|
|
1659
|
-
const allowAny = roots.includes('/');
|
|
1660
|
-
// If session requires explicit /dir pinning, block all mutations until pinned.
|
|
1661
|
-
// Exception: if cwd matches one of the repo candidates, auto-allow.
|
|
1662
|
-
if (ctx.requireDirPinForMutations && !ctx.dirPinned) {
|
|
1663
|
-
const candidates = ctx.repoCandidates ?? [];
|
|
1664
|
-
const cwdMatchesCandidate = candidates.some((c) => {
|
|
1665
|
-
const absCandidate = path.resolve(c);
|
|
1666
|
-
return absCwd === absCandidate || isWithinDir(absCwd, absCandidate);
|
|
1667
|
-
});
|
|
1668
|
-
if (!cwdMatchesCandidate) {
|
|
1669
|
-
const hint = candidates.length ? ` Candidates: ${candidates.slice(0, 8).join(', ')}` : '';
|
|
1670
|
-
throw new Error(`${tool}: BLOCKED — multiple repository candidates detected. Set repo root explicitly with /dir <path> before editing files.${hint}`);
|
|
1671
|
-
}
|
|
1672
|
-
}
|
|
1673
|
-
// Respect allowed roots first.
|
|
1674
|
-
if (roots.length > 0 && !allowAny) {
|
|
1675
|
-
const inAllowed = roots.some((root) => isWithinDir(absTarget, root));
|
|
1676
|
-
if (!inAllowed) {
|
|
1677
|
-
throw new Error(`${tool}: BLOCKED — path "${absTarget}" is outside allowed directories: ${roots.join(', ')}`);
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
// If / is explicitly allowed, permit filesystem-wide writes after pin policy above.
|
|
1681
|
-
if (allowAny)
|
|
1682
|
-
return;
|
|
1683
|
-
// In code mode, keep edits scoped to current working directory unless explicitly sys mode.
|
|
1684
|
-
if (!isWithinDir(absTarget, absCwd)) {
|
|
1685
|
-
throw new Error(`${tool}: BLOCKED — path "${absTarget}" is outside the working directory "${absCwd}". Run /dir <project-root> first, then retry with relative paths.`);
|
|
1686
|
-
}
|
|
1687
|
-
}
|
|
1408
|
+
// Path safety helpers imported from tools/path-safety.ts:
|
|
1409
|
+
// isWithinDir, resolvePath, redactPath, checkCwdWarning, enforceMutationWithinCwd
|
|
1688
1410
|
async function hasRg() {
|
|
1689
1411
|
const isWin = process.platform === 'win32';
|
|
1690
1412
|
if (!isWin) {
|