@nanhara/hara 0.121.0 → 0.122.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/CHANGELOG.md +85 -0
- package/README.md +40 -6
- package/dist/agent/failover.js +1 -1
- package/dist/agent/loop.js +158 -21
- package/dist/agent/reminders.js +22 -7
- package/dist/agent/repeat-guard.js +26 -7
- package/dist/agent/structured.js +231 -0
- package/dist/agent/touched.js +20 -6
- package/dist/config.js +62 -26
- package/dist/cron/deliver.js +37 -3
- package/dist/feedback.js +5 -9
- package/dist/fs-read.js +106 -12
- package/dist/fs-write.js +242 -16
- package/dist/gateway/dingtalk.js +4 -1
- package/dist/gateway/discord.js +53 -18
- package/dist/gateway/feishu.js +158 -57
- package/dist/gateway/flows-pending.js +720 -0
- package/dist/gateway/flows.js +391 -16
- package/dist/gateway/matrix.js +80 -15
- package/dist/gateway/mattermost.js +44 -32
- package/dist/gateway/media.js +659 -0
- package/dist/gateway/serve.js +657 -162
- package/dist/gateway/sessions.js +475 -78
- package/dist/gateway/signal.js +27 -22
- package/dist/gateway/slack.js +26 -17
- package/dist/gateway/telegram.js +32 -18
- package/dist/gateway/wecom.js +32 -24
- package/dist/gateway/weixin.js +127 -49
- package/dist/hooks.js +32 -20
- package/dist/index.js +640 -219
- package/dist/org/projects.js +347 -0
- package/dist/org/roles.js +38 -11
- package/dist/security/secrets.js +150 -0
- package/dist/serve/server.js +772 -317
- package/dist/serve/sessions.js +112 -28
- package/dist/session/store.js +337 -44
- package/dist/tools/all.js +1 -0
- package/dist/tools/builtin.js +61 -23
- package/dist/tools/codebase.js +3 -1
- package/dist/tools/computer.js +98 -92
- package/dist/tools/edit.js +11 -8
- package/dist/tools/patch.js +230 -31
- package/dist/tools/search.js +482 -72
- package/dist/tools/task.js +453 -0
- package/dist/tools/todo.js +67 -16
- package/dist/tools/web.js +364 -64
- package/dist/tui/run.js +26 -23
- package/dist/undo.js +83 -7
- package/package.json +1 -1
package/dist/undo.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// In-session undo stack for file changes. Each edit tool records the prior state of the files it
|
|
2
2
|
// touched; `/undo` pops the last group and restores it. Process-scoped (one REPL session).
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { linkSync, lstatSync, readlinkSync, symlinkSync } from "node:fs";
|
|
6
|
+
import { rename, symlink, unlink } from "node:fs/promises";
|
|
7
|
+
import { atomicWriteText, discardClaimedPath, FileChangedError, removeCreatedDirectories } from "./fs-write.js";
|
|
8
|
+
import { readRegularFileSnapshotNoFollow } from "./fs-read.js";
|
|
5
9
|
import { invalidateFileCandidates } from "./context/mentions.js";
|
|
6
10
|
const stack = [];
|
|
7
11
|
const MAX = 50;
|
|
@@ -16,27 +20,99 @@ export function recordEdit(group) {
|
|
|
16
20
|
export function undoDepth() {
|
|
17
21
|
return stack.length;
|
|
18
22
|
}
|
|
23
|
+
async function restoreMovedFile(quarantine, target) {
|
|
24
|
+
const info = lstatSync(quarantine);
|
|
25
|
+
// link(2) may follow a symlink source on some platforms. Recreate its topology explicitly while retaining
|
|
26
|
+
// create-if-absent semantics; a regular inode can still use an atomic hard link.
|
|
27
|
+
const linkTarget = info.isSymbolicLink() ? readlinkSync(quarantine) : undefined;
|
|
28
|
+
if (linkTarget !== undefined)
|
|
29
|
+
symlinkSync(linkTarget, target);
|
|
30
|
+
else
|
|
31
|
+
linkSync(quarantine, target);
|
|
32
|
+
discardClaimedPath(quarantine, {
|
|
33
|
+
dev: info.dev,
|
|
34
|
+
ino: info.ino,
|
|
35
|
+
mode: info.mode & 0o777,
|
|
36
|
+
nlink: info.nlink + (linkTarget === undefined ? 1 : 0),
|
|
37
|
+
linkTarget,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
/** Undo an update/create only if the path still names the exact inode and bytes written by that tool. */
|
|
41
|
+
async function undoCommitted(s) {
|
|
42
|
+
if (!s.committed || s.after === undefined)
|
|
43
|
+
throw new Error(`missing committed snapshot for ${s.path}`);
|
|
44
|
+
const target = s.committed.target;
|
|
45
|
+
const quarantine = join(dirname(target), `.hara-undo-${process.pid}-${randomUUID()}.tmp`);
|
|
46
|
+
await rename(target, quarantine);
|
|
47
|
+
try {
|
|
48
|
+
// O_NOFOLLOW + same-fd fstat/read rejects a path replaced with a symlink to the committed inode.
|
|
49
|
+
const current = await readRegularFileSnapshotNoFollow(quarantine);
|
|
50
|
+
if (current.dev !== s.committed.dev ||
|
|
51
|
+
current.ino !== s.committed.ino ||
|
|
52
|
+
current.mode !== s.committed.mode ||
|
|
53
|
+
current.nlink !== s.committed.nlink ||
|
|
54
|
+
current.text !== s.after) {
|
|
55
|
+
await restoreMovedFile(quarantine, target);
|
|
56
|
+
throw new FileChangedError(s.path);
|
|
57
|
+
}
|
|
58
|
+
if (s.before === null) {
|
|
59
|
+
discardClaimedPath(quarantine, s.committed);
|
|
60
|
+
await removeCreatedDirectories(s.committed.createdDirs);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
await atomicWriteText(target, s.before, { expected: null, mode: s.beforeMode });
|
|
64
|
+
discardClaimedPath(quarantine, s.committed);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
// If restoration failed after the owned inode was moved, put it back only when the destination is still
|
|
68
|
+
// absent. link(2) never overwrites a concurrent file; leave quarantine for recovery if another appeared.
|
|
69
|
+
try {
|
|
70
|
+
await restoreMovedFile(quarantine, target);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
/* the original error remains the useful user-facing failure */
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
19
78
|
/** Restore the most recent edit group. Returns the files reverted, or an error. */
|
|
20
79
|
export async function undoLast() {
|
|
21
80
|
const group = stack.pop();
|
|
22
81
|
if (!group)
|
|
23
82
|
return { error: "nothing to undo" };
|
|
24
83
|
const files = [];
|
|
84
|
+
const failures = [];
|
|
25
85
|
for (const s of group) {
|
|
26
86
|
try {
|
|
27
|
-
if (s.
|
|
28
|
-
await
|
|
87
|
+
if (s.committed && s.after !== undefined) {
|
|
88
|
+
await undoCommitted(s);
|
|
89
|
+
}
|
|
90
|
+
else if (s.before === null) {
|
|
91
|
+
try {
|
|
92
|
+
await unlink(s.absPath); // legacy snapshot: was newly created → remove
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error?.code !== "ENOENT")
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
else if (s.linkTarget !== undefined) {
|
|
100
|
+
await symlink(s.linkTarget, s.absPath); // atomic create-if-absent; never clobbers a newer path
|
|
29
101
|
}
|
|
30
102
|
else {
|
|
31
|
-
await atomicWriteText(s.absPath, s.before);
|
|
103
|
+
await atomicWriteText(s.absPath, s.before, { expected: s.removed ? null : undefined, mode: s.beforeMode });
|
|
32
104
|
}
|
|
33
105
|
files.push(s.path);
|
|
34
106
|
}
|
|
35
|
-
catch {
|
|
36
|
-
|
|
107
|
+
catch (error) {
|
|
108
|
+
failures.push(`${s.path}: ${error?.message ?? String(error)}`);
|
|
37
109
|
}
|
|
38
110
|
}
|
|
39
111
|
if (files.length)
|
|
40
112
|
invalidateFileCandidates();
|
|
113
|
+
if (failures.length) {
|
|
114
|
+
const prefix = files.length ? `partially reverted ${files.join(", ")}; ` : "";
|
|
115
|
+
return { error: `${prefix}could not safely undo ${failures.join("; ")}` };
|
|
116
|
+
}
|
|
41
117
|
return { files };
|
|
42
118
|
}
|