@orion-agents/orion-code 0.3.11 → 0.3.12
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 +47 -2
- package/README.md +8 -5
- package/README.zh-CN.md +5 -4
- package/dist/cli.js +14 -1
- package/dist/web/context-text.d.ts +17 -0
- package/dist/web/context-text.js +40 -0
- package/dist/web/file-read-service.d.ts +23 -0
- package/dist/web/file-read-service.js +134 -0
- package/dist/web/git-read-model-service.d.ts +14 -0
- package/dist/web/git-read-model-service.js +64 -0
- package/dist/web/host-daemon.d.ts +9 -0
- package/dist/web/host-daemon.js +45 -1
- package/dist/web/protocol.d.ts +7 -0
- package/dist/web/protocol.js +26 -0
- package/dist/web/review-service.d.ts +14 -0
- package/dist/web/review-service.js +34 -14
- package/dist/web/server.js +82 -0
- package/dist/web/workbench-controller.d.ts +45 -0
- package/dist/web/workbench-controller.js +84 -0
- package/dist/web-client/assets/{DiffViewer-BrE5tfZc.js → DiffViewer-Bs1C5uRM.js} +1 -1
- package/dist/web-client/assets/FilesPanel-dW61EHHn.js +2 -0
- package/dist/web-client/assets/GitPanel-ykbaVNlo.js +1 -0
- package/dist/web-client/assets/ResourceSplitLayout-B0Zs9aMC.js +1 -0
- package/dist/web-client/assets/ReviewPanel-1oqfaqoh.js +1 -0
- package/dist/web-client/assets/{TerminalPanel-CMLU7j08.js → TerminalPanel-Cz0ohyPm.js} +1 -1
- package/dist/web-client/assets/index-B11PjVU2.css +1 -0
- package/dist/web-client/assets/index-DiNz6Qhw.js +17 -0
- package/dist/web-client/index.html +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/web-client/assets/FilesPanel-ZwlVgxJi.js +0 -2
- package/dist/web-client/assets/GitPanel-Cnz52VRO.js +0 -1
- package/dist/web-client/assets/ReviewPanel-D_xxkVG_.js +0 -1
- package/dist/web-client/assets/index-BlFUf8dG.js +0 -17
- package/dist/web-client/assets/index-CUG6w669.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -22,9 +22,54 @@ which is **not** a pass.
|
|
|
22
22
|
|
|
23
23
|
## [Unreleased]
|
|
24
24
|
|
|
25
|
-
## [0.3.
|
|
25
|
+
## [0.3.12] — CANDIDATE
|
|
26
26
|
|
|
27
|
-
> **Status: candidate.**
|
|
27
|
+
> **Status: candidate.** Wide Right Workspace + repository-truth + guarded Git
|
|
28
|
+
> mutations (plan docs/plan/v0.3.12-plan.md) plus the v0.3.13 follow-ups that
|
|
29
|
+
> landed on this branch (plans v3/v4/v5). Not merged, tagged or published.
|
|
30
|
+
|
|
31
|
+
### Added
|
|
32
|
+
|
|
33
|
+
- Wide Right Workspace logic layer: five-panel registry, per-workspace layout
|
|
34
|
+
preferences (schema v4 with per-panel navigator widths), pure dock/rail/drawer
|
|
35
|
+
geometry with 320px conversation minimum and 360/560/960/max snap points.
|
|
36
|
+
- Repository snapshot store shared by Git/Review/Files decorations
|
|
37
|
+
(coalescing, one-shot invalidation, revision-stale detection).
|
|
38
|
+
- Progressive Review verification paging (GET /review/verification).
|
|
39
|
+
- Guarded Git stage/unstage/commit (POST /git/\*) with host-resolved file ids,
|
|
40
|
+
repository-revision CAS and an explicit user-gesture header.
|
|
41
|
+
- Bounded workspace search (GET /files/search; name/content scopes).
|
|
42
|
+
- Context V2 file_range references (revision-bound, line-extracted, budgeted).
|
|
43
|
+
- **Resource split layout (v0.3.13-plan-v4):** Files/Git/Review share one
|
|
44
|
+
content | splitter | navigator shell (`ResourceSplitLayout`) with explicit
|
|
45
|
+
named grid areas (no more sparse grid auto-placement gaps). Git now shows its
|
|
46
|
+
diff on the left, matching Files/Review. Each panel has an inner draggable
|
|
47
|
+
separator measured against the split root — never the workbench shell — so
|
|
48
|
+
dragging never moves the outer Right Workspace; per-workspace + per-panel
|
|
49
|
+
navigator widths are persisted (300px default, 160–420px drag range,
|
|
50
|
+
double-click/keyboard reset), and <=620px containers stack navigator-over-
|
|
51
|
+
content and hide the handle.
|
|
52
|
+
- **Conversation history navigator (v0.3.13-plan-v5):** a left-inside rail on
|
|
53
|
+
the loaded transcript derived from the unified order timeline. A pure bucket
|
|
54
|
+
model (<=48–64 stable buckets, user/assistant/errors always visible) feeds
|
|
55
|
+
decorative ticks plus one focusable `role="slider"` (Arrow/Page/Home/End,
|
|
56
|
+
reduced-motion aware). An IntersectionObserver + rAF-throttled viewport
|
|
57
|
+
marker tracks the reading position; pointer scrub uses capture and cancels
|
|
58
|
+
pinned auto-follow; the top entry drives the existing paginated
|
|
59
|
+
load-earlier. Container-query degradation: full rail >760px, compact
|
|
60
|
+
641–760px, hidden <=640px with no horizontal overflow.
|
|
61
|
+
- **Unified Orion brand mark (v0.3.13-plan-v5 §9):** one decorative
|
|
62
|
+
`OrionBrandMark` inline SVG (20×20 artboard, three bars, theme-token
|
|
63
|
+
gradients with forced-colors fallback) replaces the duplicated
|
|
64
|
+
`.brand-mark`/`.boot-mark` span CSS in the project navigator and boot
|
|
65
|
+
screen; the mark stays aria-hidden and non-interactive.
|
|
66
|
+
- E2E: rail-panel scenario ids on the allowlist; real-conversation smoke spec;
|
|
67
|
+
Files/Review/Git split-layout, history-rail and brand visual specs.
|
|
68
|
+
|
|
69
|
+
## [0.3.11] — NPM-PUBLISHED
|
|
70
|
+
|
|
71
|
+
> **Status: npm-published.** `@orion-agents/orion-code@0.3.11` is available through the npm
|
|
72
|
+
> registry (latest tag). Published 2026-09-05; git tag/release may lag.
|
|
28
73
|
|
|
29
74
|
### Added
|
|
30
75
|
|
package/README.md
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Local-first, goal-driven coding agent for the terminal and browser.
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
8
|
-
>
|
|
5
|
+
> v0.3.12 candidate — a Wide Right Workspace on top of the vertical rail: pure
|
|
6
|
+
> dock/rail/drawer geometry (detail dockable to 80vw while the conversation
|
|
7
|
+
> keeps a 320px minimum), per-workspace layout preferences, a shared repository
|
|
8
|
+
> snapshot store, progressive Review verification, guarded Git stage/unstage/
|
|
9
|
+
> commit, bounded workspace search and revision-bound file_range Context
|
|
10
|
+
> references. Candidate source is not an npm publication, Git tag, or
|
|
11
|
+
> merge-ready release.
|
|
9
12
|
|
|
10
13
|
[中文说明](README.zh-CN.md) ·
|
|
11
14
|
[v0.3.7 plan](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
|
|
@@ -170,7 +173,7 @@ current development environments. Node 20 is upstream EOL and is no longer a v0.
|
|
|
170
173
|
After the immutable `0.3.5` npm receipt exists:
|
|
171
174
|
|
|
172
175
|
```bash
|
|
173
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
176
|
+
npm install -g @orion-agents/orion-code@0.3.12
|
|
174
177
|
orion --version
|
|
175
178
|
orion doctor
|
|
176
179
|
```
|
package/README.zh-CN.md
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
面向终端与浏览器、本地优先的目标驱动 Coding Agent。
|
|
4
4
|
|
|
5
|
-
> v0.3.
|
|
6
|
-
>
|
|
7
|
-
>
|
|
5
|
+
> v0.3.12 候选版本:在竖状 rail 之上的**大尺寸右侧工作区**——纯 dock/rail/drawer 几何
|
|
6
|
+
> (详情面可拉宽至 80vw,对话保留 320px 最小宽度)、按 workspace 的布局偏好、共享仓库快照
|
|
7
|
+
> store、Review 验证证据渐进分页、受控 Git stage/unstage/commit、受限工作区搜索与带版本
|
|
8
|
+
> 约束的 file_range Context 引用。Candidate 源码不代表已创建 npm 发布、Git tag 或达到可合并状态。
|
|
8
9
|
|
|
9
10
|
[English](README.md) ·
|
|
10
11
|
[v0.3.7 方案](docs/plan/v0.3.7-left-rail-improvement-plan.md) ·
|
|
@@ -101,7 +102,7 @@ Current。Node 20 已结束上游维护,不再属于 v0.3 Runtime 合同。
|
|
|
101
102
|
当 npm 已存在不可变的 `0.3.4` 发布凭据后:
|
|
102
103
|
|
|
103
104
|
```bash
|
|
104
|
-
npm install -g @orion-agents/orion-code@0.3.
|
|
105
|
+
npm install -g @orion-agents/orion-code@0.3.12
|
|
105
106
|
orion --version
|
|
106
107
|
orion doctor
|
|
107
108
|
```
|
package/dist/cli.js
CHANGED
|
@@ -80,8 +80,21 @@ async function manageBackgroundHost(command, args) {
|
|
|
80
80
|
if (status.state === 'running') {
|
|
81
81
|
console.log(`orion web is running: ${status.pidfile.url} (pid ${status.pidfile.pid})`);
|
|
82
82
|
console.log(`workspace ${status.pidfile.workspace} · started ${new Date(status.pidfile.startedAt).toISOString()}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
// No pidfile-owned host, or a stale one. A live listener on the port means
|
|
86
|
+
// an externally managed host (e.g. a launchd LaunchAgent) is serving it —
|
|
87
|
+
// report that instead of a confusing 'not running'.
|
|
88
|
+
if (await (0, host_daemon_1.isHostPortTaken)(options.port)) {
|
|
89
|
+
console.log(`Port ${options.port} is being served by an externally managed Orion host ` +
|
|
90
|
+
`(e.g. a launchd LaunchAgent such as ai.orion-code.web). It is not owned ` +
|
|
91
|
+
`by \`orion web status/stop\`; inspect it with \`launchctl list | grep orion\`.`);
|
|
92
|
+
if (status.state === 'stale') {
|
|
93
|
+
console.log(`Note: the pidfile for the former pid ${status.pid} is stale and can be removed safely.`);
|
|
94
|
+
}
|
|
95
|
+
return;
|
|
83
96
|
}
|
|
84
|
-
|
|
97
|
+
if (status.state === 'stale') {
|
|
85
98
|
console.log(`orion web is not running (stale pidfile for pid ${status.pid} on port ${status.port}).`);
|
|
86
99
|
}
|
|
87
100
|
else {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v0.3.12 — Context text helpers shared by the controller when resolving
|
|
3
|
+
* version-bound references (file_range etc.). Pure and unit-tested.
|
|
4
|
+
*/
|
|
5
|
+
export interface ExtractedLines {
|
|
6
|
+
readonly text: string;
|
|
7
|
+
/** True when the requested range reached the end of the file before endLine. */
|
|
8
|
+
readonly clampedAtEnd: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** Extract a 1-based inclusive line range. Never throws for out-of-range input. */
|
|
11
|
+
export declare function extractLineRange(content: string, startLine: number, endLine: number): ExtractedLines;
|
|
12
|
+
/** Soft per-reference byte budget for line content sent to the model context. */
|
|
13
|
+
export declare function enforceContextBudget(text: string, maxBytes: number): {
|
|
14
|
+
readonly text: string;
|
|
15
|
+
readonly truncated: boolean;
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=context-text.d.ts.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* v0.3.12 — Context text helpers shared by the controller when resolving
|
|
4
|
+
* version-bound references (file_range etc.). Pure and unit-tested.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.extractLineRange = extractLineRange;
|
|
8
|
+
exports.enforceContextBudget = enforceContextBudget;
|
|
9
|
+
/** Extract a 1-based inclusive line range. Never throws for out-of-range input. */
|
|
10
|
+
function extractLineRange(content, startLine, endLine) {
|
|
11
|
+
const start = Math.max(1, Math.trunc(startLine));
|
|
12
|
+
const end = Math.max(start, Math.trunc(endLine));
|
|
13
|
+
const lines = content.split('\n');
|
|
14
|
+
if (start > lines.length) {
|
|
15
|
+
return Object.freeze({ text: '', clampedAtEnd: true });
|
|
16
|
+
}
|
|
17
|
+
const last = Math.min(end, lines.length);
|
|
18
|
+
const slice = lines.slice(start - 1, last);
|
|
19
|
+
return Object.freeze({
|
|
20
|
+
text: slice.join('\n'),
|
|
21
|
+
clampedAtEnd: end > lines.length,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
/** Soft per-reference byte budget for line content sent to the model context. */
|
|
25
|
+
function enforceContextBudget(text, maxBytes) {
|
|
26
|
+
if (Buffer.byteLength(text, 'utf8') <= maxBytes) {
|
|
27
|
+
return Object.freeze({ text, truncated: false });
|
|
28
|
+
}
|
|
29
|
+
let low = 0;
|
|
30
|
+
let high = text.length;
|
|
31
|
+
while (low < high) {
|
|
32
|
+
const mid = (low + high + 1) >>> 1;
|
|
33
|
+
if (Buffer.byteLength(text.slice(0, mid), 'utf8') <= maxBytes)
|
|
34
|
+
low = mid;
|
|
35
|
+
else
|
|
36
|
+
high = mid - 1;
|
|
37
|
+
}
|
|
38
|
+
return Object.freeze({ text: text.slice(0, low), truncated: true });
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=context-text.js.map
|
|
@@ -45,12 +45,35 @@ export declare class FileReadServiceV1 {
|
|
|
45
45
|
constructor(workspace: string);
|
|
46
46
|
get rootId(): string;
|
|
47
47
|
performanceCounters(): WebFileReadPerformanceCountersV1;
|
|
48
|
+
/** v0.3.12 S3 — resolve a remembered file id back to its workspace-relative path. */
|
|
49
|
+
pathForFileId(fileId: string): string;
|
|
48
50
|
identifyRelativePath(relativePath: string): string;
|
|
49
51
|
list(input?: {
|
|
50
52
|
readonly parentId?: string;
|
|
51
53
|
readonly cursor?: string;
|
|
52
54
|
readonly pageSize?: number;
|
|
53
55
|
}): WebFileTreePageV1;
|
|
56
|
+
/**
|
|
57
|
+
* v0.3.12 S4 — bounded workspace search. Name scope walks the tree (skipping
|
|
58
|
+
* VCS/dependency/ignored names, hidden directories, out-of-root symlinks and
|
|
59
|
+
* sensitive paths); content scope additionally matches inside the first
|
|
60
|
+
* 64 KiB of each text file and skips binary payloads. Results cap at
|
|
61
|
+
* `limit`; the scan stops early once the cap is reached or the file budget
|
|
62
|
+
* is exhausted.
|
|
63
|
+
*/
|
|
64
|
+
search(input: {
|
|
65
|
+
readonly query: string;
|
|
66
|
+
readonly scope: 'name' | 'content';
|
|
67
|
+
readonly limit?: number;
|
|
68
|
+
}): {
|
|
69
|
+
readonly items: ReadonlyArray<{
|
|
70
|
+
readonly id: string;
|
|
71
|
+
readonly name: string;
|
|
72
|
+
readonly path: string;
|
|
73
|
+
}>;
|
|
74
|
+
readonly truncated: boolean;
|
|
75
|
+
};
|
|
76
|
+
private fileContainsQuery;
|
|
54
77
|
readContent(input: {
|
|
55
78
|
readonly fileId: string;
|
|
56
79
|
readonly cursor?: string;
|
|
@@ -41,6 +41,14 @@ class FileReadServiceV1 {
|
|
|
41
41
|
performanceCounters() {
|
|
42
42
|
return Object.freeze({ ...this.performance });
|
|
43
43
|
}
|
|
44
|
+
/** v0.3.12 S3 — resolve a remembered file id back to its workspace-relative path. */
|
|
45
|
+
pathForFileId(fileId) {
|
|
46
|
+
const relativePath = this.pathById.get(fileId);
|
|
47
|
+
if (relativePath === undefined) {
|
|
48
|
+
throw new errors_1.WebWorkbenchError(404, 'File node was not found.', 'file_not_found');
|
|
49
|
+
}
|
|
50
|
+
return relativePath;
|
|
51
|
+
}
|
|
44
52
|
identifyRelativePath(relativePath) {
|
|
45
53
|
const normalized = normalizeRelativePath(relativePath);
|
|
46
54
|
this.resolveRelative(normalized);
|
|
@@ -107,6 +115,132 @@ class FileReadServiceV1 {
|
|
|
107
115
|
: null,
|
|
108
116
|
});
|
|
109
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* v0.3.12 S4 — bounded workspace search. Name scope walks the tree (skipping
|
|
120
|
+
* VCS/dependency/ignored names, hidden directories, out-of-root symlinks and
|
|
121
|
+
* sensitive paths); content scope additionally matches inside the first
|
|
122
|
+
* 64 KiB of each text file and skips binary payloads. Results cap at
|
|
123
|
+
* `limit`; the scan stops early once the cap is reached or the file budget
|
|
124
|
+
* is exhausted.
|
|
125
|
+
*/
|
|
126
|
+
search(input) {
|
|
127
|
+
const query = input.query.trim().slice(0, 200).toLowerCase();
|
|
128
|
+
if (!query) {
|
|
129
|
+
throw new errors_1.WebWorkbenchError(400, 'A non-empty search query is required.', 'file_search_query_required');
|
|
130
|
+
}
|
|
131
|
+
const limit = boundedPageSize(input.limit ?? 50);
|
|
132
|
+
const items = [];
|
|
133
|
+
const stack = [{ dir: this.root, rel: '' }];
|
|
134
|
+
let visitedDirectories = 0;
|
|
135
|
+
let visitedFiles = 0;
|
|
136
|
+
let truncated = false;
|
|
137
|
+
const fileBudget = 2500;
|
|
138
|
+
while (stack.length > 0 && visitedFiles < fileBudget) {
|
|
139
|
+
const { dir, rel } = stack.pop();
|
|
140
|
+
visitedDirectories += 1;
|
|
141
|
+
if (visitedDirectories > 4000) {
|
|
142
|
+
truncated = true;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
let directory;
|
|
146
|
+
try {
|
|
147
|
+
directory = (0, fs_1.opendirSync)(dir);
|
|
148
|
+
}
|
|
149
|
+
catch {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const entries = [];
|
|
153
|
+
try {
|
|
154
|
+
for (;;) {
|
|
155
|
+
const entry = directory.readSync();
|
|
156
|
+
if (!entry)
|
|
157
|
+
break;
|
|
158
|
+
if (IGNORED_NAMES.has(entry.name) ||
|
|
159
|
+
(entry.name.startsWith('.') && entry.isDirectory())) {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (entry.isDirectory())
|
|
163
|
+
entries.push({ name: entry.name, kind: 'directory' });
|
|
164
|
+
else if (entry.isFile())
|
|
165
|
+
entries.push({ name: entry.name, kind: 'file' });
|
|
166
|
+
else if (entry.isSymbolicLink())
|
|
167
|
+
entries.push({ name: entry.name, kind: 'link' });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
directory.closeSync();
|
|
172
|
+
}
|
|
173
|
+
entries.sort((left, right) => left.name.localeCompare(right.name));
|
|
174
|
+
for (const entry of entries) {
|
|
175
|
+
if (items.length >= limit) {
|
|
176
|
+
truncated = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
|
180
|
+
const absolute = (0, path_1.resolve)(dir, entry.name);
|
|
181
|
+
if (entry.kind === 'directory') {
|
|
182
|
+
stack.push({ dir: absolute, rel: childRel });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (entry.kind === 'link') {
|
|
186
|
+
try {
|
|
187
|
+
const canonical = (0, fs_1.realpathSync)(absolute);
|
|
188
|
+
if (!isWithinRoot(canonical, this.root))
|
|
189
|
+
continue;
|
|
190
|
+
if ((0, fs_1.statSync)(canonical).isDirectory())
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
visitedFiles += 1;
|
|
198
|
+
if ((0, redaction_1.isSensitiveFilePath)(childRel))
|
|
199
|
+
continue;
|
|
200
|
+
const nameMatches = entry.name.toLowerCase().includes(query);
|
|
201
|
+
if (input.scope === 'name') {
|
|
202
|
+
if (nameMatches) {
|
|
203
|
+
items.push({ id: this.remember(childRel), name: entry.name, path: childRel });
|
|
204
|
+
}
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
let contentMatches = false;
|
|
208
|
+
if (!nameMatches) {
|
|
209
|
+
contentMatches = this.fileContainsQuery(absolute, query);
|
|
210
|
+
}
|
|
211
|
+
if (nameMatches || contentMatches) {
|
|
212
|
+
items.push({ id: this.remember(childRel), name: entry.name, path: childRel });
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return Object.freeze({
|
|
217
|
+
items: Object.freeze(items.map(item => Object.freeze(item))),
|
|
218
|
+
truncated: truncated || visitedFiles >= fileBudget,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
fileContainsQuery(absolutePath, query) {
|
|
222
|
+
try {
|
|
223
|
+
const descriptor = (0, fs_1.openSync)(absolutePath, fs_1.constants.O_RDONLY);
|
|
224
|
+
try {
|
|
225
|
+
const buffer = Buffer.alloc(64 * 1024);
|
|
226
|
+
const bytesRead = (0, fs_1.readSync)(descriptor, buffer, 0, buffer.length, 0);
|
|
227
|
+
if (bytesRead === 0)
|
|
228
|
+
return false;
|
|
229
|
+
if (buffer.subarray(0, bytesRead).includes(0))
|
|
230
|
+
return false;
|
|
231
|
+
return new util_1.TextDecoder('utf-8')
|
|
232
|
+
.decode(buffer.subarray(0, bytesRead))
|
|
233
|
+
.toLowerCase()
|
|
234
|
+
.includes(query);
|
|
235
|
+
}
|
|
236
|
+
finally {
|
|
237
|
+
(0, fs_1.closeSync)(descriptor);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
110
244
|
readContent(input) {
|
|
111
245
|
this.performance.readOperations += 1;
|
|
112
246
|
const limitBytes = boundedContentBytes(input.limitBytes ?? DEFAULT_CONTENT_BYTES);
|
|
@@ -77,6 +77,20 @@ export declare class GitReadModelServiceV1 {
|
|
|
77
77
|
private capture;
|
|
78
78
|
private projectFile;
|
|
79
79
|
private rememberPath;
|
|
80
|
+
/** v0.3.12 S3 — guarded stage: paths are host-resolved file ids only. */
|
|
81
|
+
stagePaths(paths: readonly string[]): Promise<{
|
|
82
|
+
readonly repositoryRevision: string;
|
|
83
|
+
}>;
|
|
84
|
+
unstagePaths(paths: readonly string[]): Promise<{
|
|
85
|
+
readonly repositoryRevision: string;
|
|
86
|
+
}>;
|
|
87
|
+
commit(message: string): Promise<{
|
|
88
|
+
readonly repositoryRevision: string;
|
|
89
|
+
readonly commitSha: string;
|
|
90
|
+
}>;
|
|
91
|
+
private mutate;
|
|
92
|
+
private requireRepositoryRoot;
|
|
93
|
+
private revisionAfterMutation;
|
|
80
94
|
private runGit;
|
|
81
95
|
private tryGit;
|
|
82
96
|
private encodeCursor;
|
|
@@ -262,6 +262,56 @@ class GitReadModelServiceV1 {
|
|
|
262
262
|
this.pathById.set(id, path);
|
|
263
263
|
return id;
|
|
264
264
|
}
|
|
265
|
+
/** v0.3.12 S3 — guarded stage: paths are host-resolved file ids only. */
|
|
266
|
+
async stagePaths(paths) {
|
|
267
|
+
assertSafeGitPaths(paths);
|
|
268
|
+
if (paths.length === 0)
|
|
269
|
+
throw new Error('stage requires at least one path.');
|
|
270
|
+
await this.mutate(['add', '--', ...paths]);
|
|
271
|
+
return { repositoryRevision: await this.revisionAfterMutation() };
|
|
272
|
+
}
|
|
273
|
+
async unstagePaths(paths) {
|
|
274
|
+
assertSafeGitPaths(paths);
|
|
275
|
+
if (paths.length === 0)
|
|
276
|
+
throw new Error('unstage requires at least one path.');
|
|
277
|
+
await this.mutate(['reset', '-q', '--', ...paths]);
|
|
278
|
+
return { repositoryRevision: await this.revisionAfterMutation() };
|
|
279
|
+
}
|
|
280
|
+
async commit(message) {
|
|
281
|
+
const trimmed = message.trim();
|
|
282
|
+
if (!trimmed || trimmed.length > 2000 || /[\x00-\x1f]/.test(trimmed)) {
|
|
283
|
+
throw new Error('A valid commit message is required.');
|
|
284
|
+
}
|
|
285
|
+
const root = await this.requireRepositoryRoot();
|
|
286
|
+
const porcelain = (await this.runGit(['status', '--porcelain=v1'], root)).trim();
|
|
287
|
+
const stagedLines = porcelain
|
|
288
|
+
.split('\n')
|
|
289
|
+
.filter(line => line.length > 1 && line[0] !== ' ' && line[0] !== '?' && line[0] !== '!');
|
|
290
|
+
if (stagedLines.length === 0) {
|
|
291
|
+
throw new Error('Nothing is staged to commit.');
|
|
292
|
+
}
|
|
293
|
+
await this.mutate(['commit', '-m', trimmed]);
|
|
294
|
+
const sha = (await this.runGit(['rev-parse', 'HEAD'], root)).trim();
|
|
295
|
+
return { repositoryRevision: await this.revisionAfterMutation(), commitSha: sha };
|
|
296
|
+
}
|
|
297
|
+
async mutate(args) {
|
|
298
|
+
const root = await this.requireRepositoryRoot();
|
|
299
|
+
const output = await this.runGit(args, root);
|
|
300
|
+
if (output) {
|
|
301
|
+
// Warnings on stderr are not captured by runGit; treat non-empty as unexpected.
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
async requireRepositoryRoot() {
|
|
305
|
+
const snapshot = await this.capture();
|
|
306
|
+
if (!snapshot.isRepository || !snapshot.root) {
|
|
307
|
+
throw new Error('Git repository is unavailable.');
|
|
308
|
+
}
|
|
309
|
+
return snapshot.root;
|
|
310
|
+
}
|
|
311
|
+
async revisionAfterMutation() {
|
|
312
|
+
const root = await this.requireRepositoryRoot();
|
|
313
|
+
return this.runGit(['rev-parse', 'HEAD'], root).then(value => value.trim());
|
|
314
|
+
}
|
|
265
315
|
runGit(args, cwd) {
|
|
266
316
|
return new Promise((resolvePromise, reject) => {
|
|
267
317
|
this.performance.processCount += 1;
|
|
@@ -590,6 +640,20 @@ function streamGitLines(input) {
|
|
|
590
640
|
});
|
|
591
641
|
});
|
|
592
642
|
}
|
|
643
|
+
function assertSafeGitPaths(paths) {
|
|
644
|
+
for (const path of paths) {
|
|
645
|
+
if (typeof path !== 'string' ||
|
|
646
|
+
path.length === 0 ||
|
|
647
|
+
path.length > 4096 ||
|
|
648
|
+
path.includes('\0') ||
|
|
649
|
+
path.startsWith('-') ||
|
|
650
|
+
path.includes('/../') ||
|
|
651
|
+
path === '..' ||
|
|
652
|
+
path.startsWith('../')) {
|
|
653
|
+
throw new Error(`Unsafe Git path rejected: ${JSON.stringify(path)}`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
593
657
|
function gitEnvironment() {
|
|
594
658
|
return {
|
|
595
659
|
PATH: process.env.PATH,
|
|
@@ -48,6 +48,15 @@ export declare function hostDaemonStatus(port: number): HostDaemonStatus;
|
|
|
48
48
|
* stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
|
|
49
49
|
* foreground command can print the URL and exit cleanly.
|
|
50
50
|
*/
|
|
51
|
+
/**
|
|
52
|
+
* True when something already listens on 127.0.0.1:port — typically another
|
|
53
|
+
* Orion host process (e.g. one managed by launchd, which does not write a
|
|
54
|
+
* pidfile). Detecting this up front turns a raw EADDRINUSE crash into a
|
|
55
|
+
* friendly message.
|
|
56
|
+
*/
|
|
57
|
+
export declare function isHostPortTaken(port: number): Promise<boolean>;
|
|
58
|
+
/** Best-effort tail of the host log used when a background child exits early. */
|
|
59
|
+
export declare function readHostLogTail(port: number, lines?: number): string;
|
|
51
60
|
export declare function spawnBackgroundHost(options: {
|
|
52
61
|
readonly cwd: string;
|
|
53
62
|
readonly port: number;
|
package/dist/web/host-daemon.js
CHANGED
|
@@ -9,6 +9,8 @@ exports.writeHostPidfile = writeHostPidfile;
|
|
|
9
9
|
exports.clearHostPidfile = clearHostPidfile;
|
|
10
10
|
exports.isProcessAlive = isProcessAlive;
|
|
11
11
|
exports.hostDaemonStatus = hostDaemonStatus;
|
|
12
|
+
exports.isHostPortTaken = isHostPortTaken;
|
|
13
|
+
exports.readHostLogTail = readHostLogTail;
|
|
12
14
|
exports.spawnBackgroundHost = spawnBackgroundHost;
|
|
13
15
|
exports.stopBackgroundHost = stopBackgroundHost;
|
|
14
16
|
/**
|
|
@@ -26,6 +28,7 @@ exports.stopBackgroundHost = stopBackgroundHost;
|
|
|
26
28
|
* "startedAt": 1725000000000, "workspace": "/abs/path" }
|
|
27
29
|
*/
|
|
28
30
|
const child_process_1 = require("child_process");
|
|
31
|
+
const net_1 = require("net");
|
|
29
32
|
const fs_1 = require("fs");
|
|
30
33
|
const path_1 = require("path");
|
|
31
34
|
const paths_1 = require("../product/paths");
|
|
@@ -119,7 +122,48 @@ function childArgs(webArgs) {
|
|
|
119
122
|
* stdio to `~/.orion-code/logs/web-<port>.log`, and wait for the pidfile so the
|
|
120
123
|
* foreground command can print the URL and exit cleanly.
|
|
121
124
|
*/
|
|
122
|
-
|
|
125
|
+
/**
|
|
126
|
+
* True when something already listens on 127.0.0.1:port — typically another
|
|
127
|
+
* Orion host process (e.g. one managed by launchd, which does not write a
|
|
128
|
+
* pidfile). Detecting this up front turns a raw EADDRINUSE crash into a
|
|
129
|
+
* friendly message.
|
|
130
|
+
*/
|
|
131
|
+
function isHostPortTaken(port) {
|
|
132
|
+
return new Promise(resolve => {
|
|
133
|
+
const socket = (0, net_1.connect)({ host: '127.0.0.1', port });
|
|
134
|
+
let settled = false;
|
|
135
|
+
const done = (taken) => {
|
|
136
|
+
if (settled)
|
|
137
|
+
return;
|
|
138
|
+
settled = true;
|
|
139
|
+
socket.destroy();
|
|
140
|
+
resolve(taken);
|
|
141
|
+
};
|
|
142
|
+
socket.once('connect', () => done(true));
|
|
143
|
+
socket.once('error', () => done(false));
|
|
144
|
+
socket.setTimeout(800, () => done(false));
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** Best-effort tail of the host log used when a background child exits early. */
|
|
148
|
+
function readHostLogTail(port, lines = 6) {
|
|
149
|
+
try {
|
|
150
|
+
const raw = (0, fs_1.readFileSync)((0, path_1.join)(hostLogsDirectory(), `web-${port}.log`), 'utf8');
|
|
151
|
+
return raw.split('\n').filter(Boolean).slice(-lines).join('\n');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
async function assertHostPortAvailable(port) {
|
|
158
|
+
if (await isHostPortTaken(port)) {
|
|
159
|
+
throw new Error(`Port ${port} is already served by another Orion host process. This is ` +
|
|
160
|
+
`often a launchd LaunchAgent (ai.orion-code.web). Check with ` +
|
|
161
|
+
'`launchctl list | grep orion`; if the running host belongs to this ' +
|
|
162
|
+
`CLI, use \`orion web status --port ${port}\` to confirm before stopping it.`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
async function spawnBackgroundHost(options) {
|
|
166
|
+
await assertHostPortAvailable(options.port);
|
|
123
167
|
const logsDirectory = hostLogsDirectory();
|
|
124
168
|
(0, fs_1.mkdirSync)(logsDirectory, { recursive: true });
|
|
125
169
|
const logPath = (0, path_1.join)(logsDirectory, `web-${options.port}.log`);
|
package/dist/web/protocol.d.ts
CHANGED
|
@@ -171,6 +171,13 @@ export type WebContextReferenceV1 = {
|
|
|
171
171
|
readonly id: string;
|
|
172
172
|
readonly label: string;
|
|
173
173
|
readonly revision: string;
|
|
174
|
+
} | {
|
|
175
|
+
readonly kind: 'file_range';
|
|
176
|
+
readonly id: string;
|
|
177
|
+
readonly label: string;
|
|
178
|
+
readonly revision: string;
|
|
179
|
+
readonly startLine: number;
|
|
180
|
+
readonly endLine: number;
|
|
174
181
|
} | {
|
|
175
182
|
readonly kind: 'folder';
|
|
176
183
|
readonly id: string;
|
package/dist/web/protocol.js
CHANGED
|
@@ -523,6 +523,7 @@ function parseContextReferences(value) {
|
|
|
523
523
|
const row = requireRecord(entry, `contextReferences[${index}]`);
|
|
524
524
|
const kind = requireEnum(row.kind, `contextReferences[${index}].kind`, [
|
|
525
525
|
'file',
|
|
526
|
+
'file_range',
|
|
526
527
|
'folder',
|
|
527
528
|
'review',
|
|
528
529
|
'session',
|
|
@@ -534,6 +535,22 @@ function parseContextReferences(value) {
|
|
|
534
535
|
}
|
|
535
536
|
seen.add(`${kind}:${id}`);
|
|
536
537
|
const label = requireBoundedString(row.label, `contextReferences[${index}].label`, 200);
|
|
538
|
+
if (kind === 'file_range') {
|
|
539
|
+
assertOnlyKeys(row, ['kind', 'id', 'label', 'revision', 'startLine', 'endLine'], 'Context reference');
|
|
540
|
+
const startLine = requireLineNumber(row.startLine, `contextReferences[${index}].startLine`);
|
|
541
|
+
const endLine = requireLineNumber(row.endLine, `contextReferences[${index}].endLine`);
|
|
542
|
+
if (endLine < startLine) {
|
|
543
|
+
throw new WebProtocolError('file_range endLine must not precede startLine');
|
|
544
|
+
}
|
|
545
|
+
return Object.freeze({
|
|
546
|
+
kind,
|
|
547
|
+
id,
|
|
548
|
+
label,
|
|
549
|
+
revision: requireBoundedString(row.revision, `contextReferences[${index}].revision`, 256),
|
|
550
|
+
startLine,
|
|
551
|
+
endLine,
|
|
552
|
+
});
|
|
553
|
+
}
|
|
537
554
|
if (kind === 'file' || kind === 'folder') {
|
|
538
555
|
assertOnlyKeys(row, ['kind', 'id', 'label', 'revision'], 'Context reference');
|
|
539
556
|
return Object.freeze({
|
|
@@ -573,6 +590,15 @@ function assertOnlyKeys(row, keys, subject = 'command') {
|
|
|
573
590
|
if (unknown)
|
|
574
591
|
throw new WebProtocolError(`Unknown ${subject} field: ${unknown}`);
|
|
575
592
|
}
|
|
593
|
+
function requireLineNumber(value, name) {
|
|
594
|
+
if (typeof value !== 'number' ||
|
|
595
|
+
!Number.isSafeInteger(value) ||
|
|
596
|
+
value < 1 ||
|
|
597
|
+
value > 100000000) {
|
|
598
|
+
throw new WebProtocolError(`${name} must be an integer line number from 1 through 100000000`);
|
|
599
|
+
}
|
|
600
|
+
return value;
|
|
601
|
+
}
|
|
576
602
|
function requireBoundedString(value, name, maxLength) {
|
|
577
603
|
if (typeof value !== 'string' || !value.trim()) {
|
|
578
604
|
throw new WebProtocolError(`${name} must be a non-empty string`);
|
|
@@ -34,6 +34,20 @@ export declare class ReviewServiceV1 {
|
|
|
34
34
|
private readonly listReceiptRefs;
|
|
35
35
|
constructor(git: GitReadModelServiceV1, listReceiptRefs: () => Promise<readonly VerifiedDurableToolReceiptRefV1[]> | readonly VerifiedDurableToolReceiptRefV1[]);
|
|
36
36
|
snapshot(): Promise<WebReviewSnapshotV1>;
|
|
37
|
+
/**
|
|
38
|
+
* v0.3.12 S2 — progressive verification evidence. The Review summary loads
|
|
39
|
+
* first; receipts page in behind it, optionally filtered to one session.
|
|
40
|
+
*/
|
|
41
|
+
verificationPage(input: {
|
|
42
|
+
readonly sessionId?: string;
|
|
43
|
+
readonly cursor?: number;
|
|
44
|
+
readonly pageSize?: number;
|
|
45
|
+
}): Promise<{
|
|
46
|
+
readonly items: readonly WebReviewVerificationV1[];
|
|
47
|
+
readonly nextCursor: number | null;
|
|
48
|
+
readonly totalForSession: number;
|
|
49
|
+
}>;
|
|
50
|
+
private projectVerification;
|
|
37
51
|
diff(input: Parameters<GitReadModelServiceV1['diff']>[0]): Promise<WebGitDiffPageV1>;
|
|
38
52
|
}
|
|
39
53
|
//# sourceMappingURL=review-service.d.ts.map
|