s-app-scoder-agent 0.5.0 → 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/.scoderrc.example.json +97 -0
- package/LICENSE +21 -21
- package/README.md +599 -435
- package/dist/agent.js +829 -168
- package/dist/agents.js +70 -0
- package/dist/compaction.js +114 -0
- package/dist/config.js +178 -12
- package/dist/context.js +119 -14
- package/dist/cost.js +56 -6
- package/dist/guardrails.js +409 -0
- package/dist/index.js +216 -45
- package/dist/json-schema.js +58 -0
- package/dist/llm.js +148 -11
- package/dist/mcp.js +397 -70
- package/dist/model-catalog.js +175 -0
- package/dist/permissions.js +328 -35
- package/dist/prompt-builder.js +47 -0
- package/dist/prompt.js +97 -0
- package/dist/provider-anthropic.js +113 -35
- package/dist/provider-registry.js +116 -0
- package/dist/retry.js +3 -2
- package/dist/sandbox.js +96 -0
- package/dist/server.js +28 -1
- package/dist/session.js +282 -11
- package/dist/shell-scanner.js +112 -0
- package/dist/skills.js +122 -0
- package/dist/snapshot.js +170 -0
- package/dist/steering.js +35 -0
- package/dist/tools.js +537 -24
- package/dist/tui/app.js +615 -0
- package/dist/tui/at-complete.js +93 -0
- package/dist/tui/chat-views.js +145 -0
- package/dist/tui/chrome.js +96 -0
- package/dist/tui/command-registry.js +189 -0
- package/dist/tui/commands.js +222 -0
- package/dist/tui/image.js +149 -0
- package/dist/tui/input.js +214 -0
- package/dist/tui/markdown.js +229 -0
- package/dist/tui/mouse.js +20 -0
- package/dist/tui/permission-modal.js +34 -0
- package/dist/tui/review.js +64 -0
- package/dist/tui/session-picker.js +68 -0
- package/dist/tui/sidebar.js +101 -0
- package/dist/tui/state.js +342 -0
- package/dist/tui/theme.js +90 -0
- package/dist/tui/vim.js +60 -0
- package/dist/ui.js +43 -3
- package/dist/watch.js +52 -0
- package/package.json +18 -5
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Snapshot & undo — git shadow repository (H-11).
|
|
3
|
+
*
|
|
4
|
+
* Source: anomalyco/opencode (packages/core/src/snapshot.ts) — pattern
|
|
5
|
+
* adapted: a separate git directory under .scoder/snapshot/ shares the
|
|
6
|
+
* project worktree; each write-batch captures a checkpoint commit. `git`
|
|
7
|
+
* binary required; best-effort (failures never crash the run).
|
|
8
|
+
*
|
|
9
|
+
* Layout: .scoder/snapshot/ (GIT_DIR, bare-ish) + checkpoint log in memory.
|
|
10
|
+
* Restore: `git checkout <tree> -- .` onto the worktree via the shadow.
|
|
11
|
+
*/
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
15
|
+
/** True when `git` is on PATH and the workdir is inside a git repo (or can be). */
|
|
16
|
+
function gitAvailable(workdir) {
|
|
17
|
+
const r = spawnSync("git", ["rev-parse", "--git-dir"], { cwd: workdir, encoding: "utf8" });
|
|
18
|
+
return r.status === 0;
|
|
19
|
+
}
|
|
20
|
+
export class SnapshotManager {
|
|
21
|
+
gitDir;
|
|
22
|
+
workdir;
|
|
23
|
+
enabled;
|
|
24
|
+
checkpoints = [];
|
|
25
|
+
initialized = false;
|
|
26
|
+
available = false;
|
|
27
|
+
/** Set when the project has no .git — fallback: no-op (copy-tree skipped, see ponytail). */
|
|
28
|
+
degradedReason = null;
|
|
29
|
+
constructor(workdir, enabled) {
|
|
30
|
+
this.workdir = resolve(workdir);
|
|
31
|
+
this.gitDir = join(this.workdir, ".scoder", "snapshot");
|
|
32
|
+
this.enabled = enabled;
|
|
33
|
+
if (!enabled) {
|
|
34
|
+
this.degradedReason = null;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
if (!gitAvailable(this.workdir)) {
|
|
39
|
+
this.degradedReason = "git binary unavailable or not a repo — snapshots disabled (fallback copy-tree not enabled)";
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
mkdirSync(this.gitDir, { recursive: true });
|
|
43
|
+
this.available = true;
|
|
44
|
+
}
|
|
45
|
+
catch (err) {
|
|
46
|
+
this.degradedReason = err.message;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/** Lazily init the shadow repo (first capture only). */
|
|
50
|
+
init() {
|
|
51
|
+
if (this.initialized)
|
|
52
|
+
return this.available;
|
|
53
|
+
this.initialized = true;
|
|
54
|
+
if (!this.available)
|
|
55
|
+
return false;
|
|
56
|
+
const ok = (args) => spawnSync("git", args, { cwd: this.workdir, env: { ...process.env, GIT_DIR: this.gitDir, GIT_WORK_TREE: this.workdir }, encoding: "utf8" });
|
|
57
|
+
try {
|
|
58
|
+
if (!existsSync(join(this.gitDir, "HEAD"))) {
|
|
59
|
+
if (ok(["init", "--quiet", this.gitDir]).status !== 0) {
|
|
60
|
+
this.available = false;
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
// Ignore everything except what we track: snapshot repo has its own index.
|
|
65
|
+
this.available = true;
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
this.available = false;
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** Capture current state as a checkpoint. Returns the tree hash or null. */
|
|
74
|
+
capture(label) {
|
|
75
|
+
if (!this.enabled || !this.init())
|
|
76
|
+
return null;
|
|
77
|
+
const run = (args) => spawnSync("git", args, {
|
|
78
|
+
cwd: this.workdir,
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
env: { ...process.env, GIT_DIR: this.gitDir, GIT_WORK_TREE: this.workdir },
|
|
81
|
+
});
|
|
82
|
+
try {
|
|
83
|
+
// Add all (shadow index is independent from the project's index).
|
|
84
|
+
if (run(["add", "-A", "--", ".", ":(exclude).scoder"]).status !== 0)
|
|
85
|
+
return null;
|
|
86
|
+
const tree = run(["write-tree"]);
|
|
87
|
+
if (tree.status !== 0)
|
|
88
|
+
return null;
|
|
89
|
+
const hash = tree.stdout.trim();
|
|
90
|
+
// Empty tree (nothing changed vs previous checkpoint) — still record.
|
|
91
|
+
const ts = new Date().toISOString();
|
|
92
|
+
const id = `cp${this.checkpoints.length + 1}-${hash.slice(0, 8)}`;
|
|
93
|
+
const entry = { id, tree: hash, ts, label };
|
|
94
|
+
this.checkpoints.push(entry);
|
|
95
|
+
return entry;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** List checkpoints (newest last). */
|
|
102
|
+
list() {
|
|
103
|
+
return [...this.checkpoints];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* T-06: diff between two checkpoint trees (or tree vs current worktree
|
|
107
|
+
* when `to` is null). Returns unified diff text or null.
|
|
108
|
+
*/
|
|
109
|
+
diff(fromTree, toTree) {
|
|
110
|
+
if (!this.enabled || !this.init())
|
|
111
|
+
return null;
|
|
112
|
+
const run = (args) => spawnSync("git", args, {
|
|
113
|
+
cwd: this.workdir,
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
env: { ...process.env, GIT_DIR: this.gitDir, GIT_WORK_TREE: this.workdir },
|
|
116
|
+
});
|
|
117
|
+
const args = toTree ? ["diff-tree", "-p", fromTree, toTree] : ["diff", fromTree];
|
|
118
|
+
const r = run(args);
|
|
119
|
+
if (r.status !== 0 && r.stdout.length === 0)
|
|
120
|
+
return null;
|
|
121
|
+
return r.stdout || null;
|
|
122
|
+
}
|
|
123
|
+
/** Revert tracked files to a checkpoint tree. Files added after it are deleted. */
|
|
124
|
+
restore(tree) {
|
|
125
|
+
if (!this.enabled || !this.init())
|
|
126
|
+
return false;
|
|
127
|
+
const run = (args) => spawnSync("git", args, {
|
|
128
|
+
cwd: this.workdir,
|
|
129
|
+
encoding: "utf8",
|
|
130
|
+
env: { ...process.env, GIT_DIR: this.gitDir, GIT_WORK_TREE: this.workdir },
|
|
131
|
+
});
|
|
132
|
+
try {
|
|
133
|
+
// Index → checkpoint tree, then force-checkout all index entries.
|
|
134
|
+
if (run(["read-tree", tree]).status !== 0)
|
|
135
|
+
return false;
|
|
136
|
+
if (run(["checkout-index", "-a", "-f"]).status !== 0)
|
|
137
|
+
return false;
|
|
138
|
+
// Delete files present in the workdir but absent from the checkpoint
|
|
139
|
+
// (checkout-index cannot remove). Diff tree vs workdir via ls-files.
|
|
140
|
+
const known = run(["ls-tree", "-r", "--name-only", tree]);
|
|
141
|
+
if (known.status !== 0)
|
|
142
|
+
return false;
|
|
143
|
+
const treeFiles = new Set(known.stdout.trim().split("\n").filter(Boolean));
|
|
144
|
+
const walk = (dir, rel) => {
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
for (const e of entries) {
|
|
153
|
+
if (e.name === ".git" || e.name === ".scoder")
|
|
154
|
+
continue;
|
|
155
|
+
const abs = join(dir, e.name);
|
|
156
|
+
const r = rel ? `${rel}/${e.name}` : e.name;
|
|
157
|
+
if (e.isDirectory())
|
|
158
|
+
walk(abs, r);
|
|
159
|
+
else if (!treeFiles.has(r))
|
|
160
|
+
rmSync(abs, { force: true });
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
walk(this.workdir, "");
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
package/dist/steering.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* I5: Steering inbox tách module — policy delivery tại safe step boundary
|
|
3
|
+
* (pattern từ opencode session/runner). Thread-safe trong 1 event loop:
|
|
4
|
+
* push từ TUI/serve, drain ở đầu mỗi step của runLoop. Không rơi message:
|
|
5
|
+
* drain nguyên batch (FIFO), steer giữa parallel tool calls chờ tới boundary.
|
|
6
|
+
*/
|
|
7
|
+
export class SteeringInbox {
|
|
8
|
+
items = [];
|
|
9
|
+
/** Steer: deliver ở NEXT step boundary (kể cả khi đang chạy tool). */
|
|
10
|
+
steer(text) {
|
|
11
|
+
const t = text.trim();
|
|
12
|
+
if (!t)
|
|
13
|
+
return;
|
|
14
|
+
this.items.push({ mode: "steer", text: t });
|
|
15
|
+
}
|
|
16
|
+
/** Queue: same inbox — caller phân biệt mode chỉ để log/event. */
|
|
17
|
+
queue(text) {
|
|
18
|
+
const t = text.trim();
|
|
19
|
+
if (!t)
|
|
20
|
+
return;
|
|
21
|
+
this.items.push({ mode: "queue", text: t });
|
|
22
|
+
}
|
|
23
|
+
/** Drain toàn bộ (FIFO) — một batch cho step boundary này. */
|
|
24
|
+
drain() {
|
|
25
|
+
const out = this.items;
|
|
26
|
+
this.items = [];
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
get size() {
|
|
30
|
+
return this.items.length;
|
|
31
|
+
}
|
|
32
|
+
clear() {
|
|
33
|
+
this.items = [];
|
|
34
|
+
}
|
|
35
|
+
}
|