codebase-onboarder 0.7.0 → 1.0.1
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/README.md +140 -12
- package/bin/onboarder.js +0 -0
- package/cli/agent/command.d.ts +71 -0
- package/cli/agent/command.js +177 -0
- package/cli/agent/command.js.map +1 -0
- package/cli/agent/config.d.ts +31 -0
- package/cli/agent/config.js +98 -0
- package/cli/agent/config.js.map +1 -0
- package/cli/agent/contracts.d.ts +77 -0
- package/cli/agent/contracts.js +2 -0
- package/cli/agent/contracts.js.map +1 -0
- package/cli/agent/files.d.ts +19 -0
- package/cli/agent/files.js +109 -0
- package/cli/agent/files.js.map +1 -0
- package/cli/agent/gitSource.d.ts +18 -0
- package/cli/agent/gitSource.js +69 -0
- package/cli/agent/gitSource.js.map +1 -0
- package/cli/agent/github.d.ts +21 -0
- package/cli/agent/github.js +94 -0
- package/cli/agent/github.js.map +1 -0
- package/cli/agent/mcp.d.ts +15 -0
- package/cli/agent/mcp.js +78 -0
- package/cli/agent/mcp.js.map +1 -0
- package/cli/agent/process.d.ts +18 -0
- package/cli/agent/process.js +118 -0
- package/cli/agent/process.js.map +1 -0
- package/cli/agent/protocol.d.ts +3 -0
- package/cli/agent/protocol.js +26 -0
- package/cli/agent/protocol.js.map +1 -0
- package/cli/agent/runner.d.ts +34 -0
- package/cli/agent/runner.js +152 -0
- package/cli/agent/runner.js.map +1 -0
- package/cli/agent/stdio.d.ts +1 -0
- package/cli/agent/stdio.js +62 -0
- package/cli/agent/stdio.js.map +1 -0
- package/cli/agent/tools.d.ts +7 -0
- package/cli/agent/tools.js +225 -0
- package/cli/agent/tools.js.map +1 -0
- package/cli/chat/analysis.d.ts +26 -0
- package/cli/chat/analysis.js +188 -0
- package/cli/chat/analysis.js.map +1 -0
- package/cli/chat/app.d.ts +16 -0
- package/cli/chat/app.js +363 -0
- package/cli/chat/app.js.map +1 -0
- package/cli/chat/commands.d.ts +11 -0
- package/cli/chat/commands.js +122 -0
- package/cli/chat/commands.js.map +1 -0
- package/cli/chat/composer.d.ts +75 -0
- package/cli/chat/composer.js +467 -0
- package/cli/chat/composer.js.map +1 -0
- package/cli/chat/controller.d.ts +64 -0
- package/cli/chat/controller.js +402 -0
- package/cli/chat/controller.js.map +1 -0
- package/cli/chat/logo.d.ts +3 -0
- package/cli/chat/logo.js +14 -0
- package/cli/chat/logo.js.map +1 -0
- package/cli/chat/models.d.ts +18 -0
- package/cli/chat/models.js +88 -0
- package/cli/chat/models.js.map +1 -0
- package/cli/chat/profile.d.ts +5 -0
- package/cli/chat/profile.js +98 -0
- package/cli/chat/profile.js.map +1 -0
- package/cli/chat/render.d.ts +40 -0
- package/cli/chat/render.js +259 -0
- package/cli/chat/render.js.map +1 -0
- package/cli/chat/screen.d.ts +26 -0
- package/cli/chat/screen.js +117 -0
- package/cli/chat/screen.js.map +1 -0
- package/cli/chat/source.d.ts +29 -0
- package/cli/chat/source.js +116 -0
- package/cli/chat/source.js.map +1 -0
- package/cli/chat/store.d.ts +43 -0
- package/cli/chat/store.js +94 -0
- package/cli/chat/store.js.map +1 -0
- package/cli/chat/text.d.ts +4 -0
- package/cli/chat/text.js +27 -0
- package/cli/chat/text.js.map +1 -0
- package/cli/explorer/app.d.ts +1 -0
- package/cli/explorer/app.js +20 -1
- package/cli/explorer/app.js.map +1 -1
- package/cli/explorer/commands.js +5 -0
- package/cli/explorer/commands.js.map +1 -1
- package/cli/explorer/featureViews.d.ts +1 -0
- package/cli/explorer/featureViews.js +4 -0
- package/cli/explorer/featureViews.js.map +1 -1
- package/cli/main.js +47 -13
- package/cli/main.js.map +1 -1
- package/docs/HERMES_RESEARCH.md +51 -0
- package/package.json +2 -1
- package/public/app.js +98 -24
- package/public/app.js.map +1 -1
- package/public/assets/onboarder-mark.svg +11 -0
- package/public/browser.d.ts +3 -0
- package/public/browser.js.map +1 -1
- package/public/index.html +24 -7
- package/public/js/about.js +2 -2
- package/public/js/about.js.map +1 -1
- package/public/js/aiDraft.d.ts +1 -0
- package/public/js/aiDraft.js +66 -17
- package/public/js/aiDraft.js.map +1 -1
- package/public/js/api.d.ts +8 -5
- package/public/js/api.js +45 -22
- package/public/js/api.js.map +1 -1
- package/public/js/deepAnalysisView.js +2 -2
- package/public/js/deepAnalysisView.js.map +1 -1
- package/public/js/diagramPane.d.ts +1 -0
- package/public/js/diagramPane.js +5 -0
- package/public/js/diagramPane.js.map +1 -1
- package/public/js/docsView.js +6 -6
- package/public/js/docsView.js.map +1 -1
- package/public/js/dom.d.ts +8 -0
- package/public/js/dom.js.map +1 -1
- package/public/js/llm.d.ts +3 -1
- package/public/js/llm.js +15 -8
- package/public/js/llm.js.map +1 -1
- package/public/js/repoFiles.js +1 -1
- package/public/js/repoFiles.js.map +1 -1
- package/public/js/reviewView.js +1 -1
- package/public/js/reviewView.js.map +1 -1
- package/public/styles.css +25 -0
- package/server/gitDiff.d.ts +1 -1
- package/server/hermesChat.d.ts +13 -0
- package/server/hermesChat.js +145 -0
- package/server/hermesChat.js.map +1 -0
- package/server/http.d.ts +5 -4
- package/server/http.js +0 -4
- package/server/http.js.map +1 -1
- package/server/mcp/server.d.ts +1 -1
- package/server/mcp/server.js +2 -2
- package/server/mcp/server.js.map +1 -1
- package/server/router.js +7 -3
- package/server/router.js.map +1 -1
- package/server/tools/scan.js +16 -2
- package/server/tools/scan.js.map +1 -1
- package/server/tools.d.ts +1 -0
- package/server/tools.js +46 -6
- package/server/tools.js.map +1 -1
- package/shared/diagram/aiMermaid.js +16 -13
- package/shared/diagram/aiMermaid.js.map +1 -1
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { graphemes, clusterCells } from './text.js';
|
|
2
|
+
import { terminalText } from '../agent/process.js';
|
|
3
|
+
/** Keep only our text styling; wrap by terminal cells, including emoji and CJK. */
|
|
4
|
+
export function wrapScreenLine(text, columns) {
|
|
5
|
+
const room = Math.max(1, columns), rows = [];
|
|
6
|
+
let row = '', used = 0, style = '';
|
|
7
|
+
for (const token of text.split(/(\x1b\[[\d;]*m)/g)) {
|
|
8
|
+
if (/^\x1b\[[\d;]*m$/.test(token)) {
|
|
9
|
+
row += token;
|
|
10
|
+
style = token === '\x1b[0m' || token === '\x1b[m' ? '' : (style + token).slice(-512);
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
for (const cluster of graphemes(terminalText(token).replace(/\t/g, ' ').replace(/[\r\n]/g, ' '))) {
|
|
14
|
+
const size = clusterCells(cluster);
|
|
15
|
+
if (used + size > room && used) {
|
|
16
|
+
rows.push(row + (style ? '\x1b[0m' : ''));
|
|
17
|
+
row = style;
|
|
18
|
+
used = 0;
|
|
19
|
+
}
|
|
20
|
+
row += size > room ? '?' : cluster;
|
|
21
|
+
used += Math.min(size, room);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
rows.push(row + (style ? '\x1b[0m' : ''));
|
|
25
|
+
return rows;
|
|
26
|
+
}
|
|
27
|
+
/** Alternate-screen ownership, bounded transcript and updates of changed rows only. */
|
|
28
|
+
export class TerminalScreen {
|
|
29
|
+
output;
|
|
30
|
+
lines = [];
|
|
31
|
+
bytes = 0;
|
|
32
|
+
width = 0;
|
|
33
|
+
wrapped = [];
|
|
34
|
+
dirty = true;
|
|
35
|
+
offset = 0;
|
|
36
|
+
frame = [];
|
|
37
|
+
columns = 0;
|
|
38
|
+
active = false;
|
|
39
|
+
pendingRows = 0;
|
|
40
|
+
constructor(output) {
|
|
41
|
+
this.output = output;
|
|
42
|
+
}
|
|
43
|
+
get scrolled() { return this.offset > 0; }
|
|
44
|
+
enter() {
|
|
45
|
+
if (this.active)
|
|
46
|
+
return;
|
|
47
|
+
this.active = true;
|
|
48
|
+
this.frame = [];
|
|
49
|
+
this.columns = 0;
|
|
50
|
+
this.output.write('\x1b[?1049h\x1b[2J\x1b[H');
|
|
51
|
+
}
|
|
52
|
+
leave() {
|
|
53
|
+
if (!this.active)
|
|
54
|
+
return;
|
|
55
|
+
this.active = false;
|
|
56
|
+
this.frame = [];
|
|
57
|
+
this.output.write('\x1b[0m\x1b[?25h\x1b[?1049l');
|
|
58
|
+
}
|
|
59
|
+
append(text) {
|
|
60
|
+
// Keep source and code whitespace, while bounding the in-memory viewport.
|
|
61
|
+
// Complete conversations are still persisted by the controller.
|
|
62
|
+
const lines = text.split(/\r?\n/);
|
|
63
|
+
if (this.offset && this.width)
|
|
64
|
+
this.pendingRows += lines.reduce((n, line) => n + wrapScreenLine(line, this.width).length, 0);
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
this.lines.push(line);
|
|
67
|
+
this.bytes += Buffer.byteLength(line);
|
|
68
|
+
}
|
|
69
|
+
while (this.lines.length > 4000 || this.bytes > 2 * 1024 * 1024)
|
|
70
|
+
this.bytes -= Buffer.byteLength(this.lines.shift());
|
|
71
|
+
this.dirty = true;
|
|
72
|
+
}
|
|
73
|
+
clear() { this.lines = []; this.bytes = 0; this.offset = 0; this.pendingRows = 0; this.dirty = true; }
|
|
74
|
+
reflow(columns) {
|
|
75
|
+
if (!this.dirty && this.width === columns)
|
|
76
|
+
return;
|
|
77
|
+
this.wrapped = this.lines.flatMap(line => wrapScreenLine(line, columns));
|
|
78
|
+
this.width = columns;
|
|
79
|
+
this.dirty = false;
|
|
80
|
+
this.offset += this.pendingRows;
|
|
81
|
+
this.pendingRows = 0;
|
|
82
|
+
}
|
|
83
|
+
scroll(direction, columns, height) {
|
|
84
|
+
this.reflow(columns);
|
|
85
|
+
this.offset = Math.min(Math.max(0, this.wrapped.length - height), Math.max(0, this.offset + direction * Math.max(1, height - 2)));
|
|
86
|
+
}
|
|
87
|
+
view(columns, height, welcome) {
|
|
88
|
+
this.reflow(columns);
|
|
89
|
+
this.offset = Math.min(this.offset, Math.max(0, this.wrapped.length - height));
|
|
90
|
+
if (!this.lines.length)
|
|
91
|
+
return welcome.split('\n').flatMap(line => wrapScreenLine(line, columns)).slice(0, height);
|
|
92
|
+
const end = Math.max(0, this.wrapped.length - this.offset);
|
|
93
|
+
return this.wrapped.slice(Math.max(0, end - height), end);
|
|
94
|
+
}
|
|
95
|
+
draw(rows, columns, cursorRow, cursorColumn) {
|
|
96
|
+
if (!this.active)
|
|
97
|
+
return;
|
|
98
|
+
let write = '';
|
|
99
|
+
const resized = columns !== this.columns || rows.length !== this.frame.length;
|
|
100
|
+
if (resized) {
|
|
101
|
+
write += '\x1b[2J';
|
|
102
|
+
this.frame = [];
|
|
103
|
+
this.columns = columns;
|
|
104
|
+
}
|
|
105
|
+
rows.forEach((row, i) => {
|
|
106
|
+
if (row !== this.frame[i])
|
|
107
|
+
write += `\x1b[${i + 1};1H\x1b[2K` + row;
|
|
108
|
+
});
|
|
109
|
+
// Cursor placement remains stable through resizing, streamed output and menus.
|
|
110
|
+
if (write)
|
|
111
|
+
this.output.write('\x1b[?25l' + write + `\x1b[${cursorRow + 1};${cursorColumn + 1}H\x1b[?25h`);
|
|
112
|
+
else
|
|
113
|
+
this.output.write(`\x1b[${cursorRow + 1};${cursorColumn + 1}H`);
|
|
114
|
+
this.frame = rows;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
//# sourceMappingURL=screen.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"screen.js","sourceRoot":"","sources":["../../src/cli/chat/screen.ts"],"names":[],"mappings":"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","sourcesContent":["import { graphemes, clusterCells } from './text.js';\nimport { terminalText } from '../agent/process.js';\n\n/** Keep only our text styling; wrap by terminal cells, including emoji and CJK. */\nexport function wrapScreenLine(text: string, columns: number): string[] {\n const room = Math.max(1, columns), rows: string[] = [];\n let row = '', used = 0, style = '';\n for (const token of text.split(/(\\x1b\\[[\\d;]*m)/g)) {\n if (/^\\x1b\\[[\\d;]*m$/.test(token)) {\n row += token;\n style = token === '\\x1b[0m' || token === '\\x1b[m' ? '' : (style + token).slice(-512);\n continue;\n }\n for (const cluster of graphemes(terminalText(token).replace(/\\t/g, ' ').replace(/[\\r\\n]/g, ' '))) {\n const size = clusterCells(cluster);\n if (used + size > room && used) { rows.push(row + (style ? '\\x1b[0m' : '')); row = style; used = 0; }\n row += size > room ? '?' : cluster; used += Math.min(size, room);\n }\n }\n rows.push(row + (style ? '\\x1b[0m' : ''));\n return rows;\n}\n\n/** Alternate-screen ownership, bounded transcript and updates of changed rows only. */\nexport class TerminalScreen {\n private lines: string[] = [];\n private bytes = 0;\n private width = 0;\n private wrapped: string[] = [];\n private dirty = true;\n private offset = 0;\n private frame: string[] = [];\n private columns = 0;\n private active = false;\n private pendingRows = 0;\n constructor(private output: NodeJS.WriteStream) {}\n get scrolled() { return this.offset > 0; }\n enter() {\n if (this.active) return;\n this.active = true; this.frame = []; this.columns = 0;\n this.output.write('\\x1b[?1049h\\x1b[2J\\x1b[H');\n }\n leave() {\n if (!this.active) return;\n this.active = false; this.frame = [];\n this.output.write('\\x1b[0m\\x1b[?25h\\x1b[?1049l');\n }\n append(text: string) {\n // Keep source and code whitespace, while bounding the in-memory viewport.\n // Complete conversations are still persisted by the controller.\n const lines = text.split(/\\r?\\n/);\n if (this.offset && this.width) this.pendingRows += lines.reduce((n, line) => n + wrapScreenLine(line, this.width).length, 0);\n for (const line of lines) { this.lines.push(line); this.bytes += Buffer.byteLength(line); }\n while (this.lines.length > 4000 || this.bytes > 2 * 1024 * 1024) this.bytes -= Buffer.byteLength(this.lines.shift()!);\n this.dirty = true;\n }\n clear() { this.lines = []; this.bytes = 0; this.offset = 0; this.pendingRows = 0; this.dirty = true; }\n private reflow(columns: number) {\n if (!this.dirty && this.width === columns) return;\n this.wrapped = this.lines.flatMap(line => wrapScreenLine(line, columns));\n this.width = columns; this.dirty = false;\n this.offset += this.pendingRows; this.pendingRows = 0;\n }\n scroll(direction: number, columns: number, height: number) {\n this.reflow(columns);\n this.offset = Math.min(Math.max(0, this.wrapped.length - height), Math.max(0, this.offset + direction * Math.max(1, height - 2)));\n }\n view(columns: number, height: number, welcome: string): string[] {\n this.reflow(columns);\n this.offset = Math.min(this.offset, Math.max(0, this.wrapped.length - height));\n if (!this.lines.length) return welcome.split('\\n').flatMap(line => wrapScreenLine(line, columns)).slice(0, height);\n const end = Math.max(0, this.wrapped.length - this.offset);\n return this.wrapped.slice(Math.max(0, end - height), end);\n }\n draw(rows: string[], columns: number, cursorRow: number, cursorColumn: number) {\n if (!this.active) return;\n let write = '';\n const resized = columns !== this.columns || rows.length !== this.frame.length;\n if (resized) { write += '\\x1b[2J'; this.frame = []; this.columns = columns; }\n rows.forEach((row, i) => {\n if (row !== this.frame[i]) write += `\\x1b[${i + 1};1H\\x1b[2K` + row;\n });\n // Cursor placement remains stable through resizing, streamed output and menus.\n if (write) this.output.write('\\x1b[?25l' + write + `\\x1b[${cursorRow + 1};${cursorColumn + 1}H\\x1b[?25h`);\n else this.output.write(`\\x1b[${cursorRow + 1};${cursorColumn + 1}H`);\n this.frame = rows;\n }\n}\n"]}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { runProcess } from '../agent/process.js';
|
|
2
|
+
export interface ChatSource {
|
|
3
|
+
root: string;
|
|
4
|
+
sourceUrl?: string;
|
|
5
|
+
reused: boolean;
|
|
6
|
+
}
|
|
7
|
+
interface SourceOptions {
|
|
8
|
+
env?: NodeJS.ProcessEnv;
|
|
9
|
+
execute?: typeof runProcess;
|
|
10
|
+
}
|
|
11
|
+
export declare function isRemoteSource(value: string): boolean;
|
|
12
|
+
export declare function githubSource(value: string): {
|
|
13
|
+
repository: string;
|
|
14
|
+
url: string;
|
|
15
|
+
gitUrl: string;
|
|
16
|
+
};
|
|
17
|
+
/** Persistent checkouts keep conversations and agent worktrees usable after exit. */
|
|
18
|
+
export declare class ChatSources {
|
|
19
|
+
readonly home: string;
|
|
20
|
+
private options;
|
|
21
|
+
constructor(home?: string, options?: SourceOptions);
|
|
22
|
+
get directory(): string;
|
|
23
|
+
private target;
|
|
24
|
+
private git;
|
|
25
|
+
private verify;
|
|
26
|
+
open(target: string, from?: string, signal?: AbortSignal, progress?: (text: string) => void): Promise<ChatSource>;
|
|
27
|
+
pull(root: string, signal?: AbortSignal, progress?: (text: string) => void): Promise<string>;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
5
|
+
import { agentHome } from '../agent/config.js';
|
|
6
|
+
import { repositoryFromRemote } from '../agent/github.js';
|
|
7
|
+
import { runProcess } from '../agent/process.js';
|
|
8
|
+
export function isRemoteSource(value) { return /^(?:https?:\/\/|ssh:\/\/|git@)/i.test(value); }
|
|
9
|
+
export function githubSource(value) {
|
|
10
|
+
const raw = value.trim(), repository = repositoryFromRemote(raw);
|
|
11
|
+
if (!repository)
|
|
12
|
+
throw new Error('Enter a GitHub repository URL: https://github.com/owner/repo or git@github.com:owner/repo.git.');
|
|
13
|
+
const url = `https://github.com/${repository}`;
|
|
14
|
+
const gitUrl = /^https:/i.test(raw) ? url + '.git' : `git@github.com:${repository}.git`;
|
|
15
|
+
return { repository, url, gitUrl };
|
|
16
|
+
}
|
|
17
|
+
/** Persistent checkouts keep conversations and agent worktrees usable after exit. */
|
|
18
|
+
export class ChatSources {
|
|
19
|
+
home;
|
|
20
|
+
options;
|
|
21
|
+
constructor(home = agentHome(), options = {}) {
|
|
22
|
+
this.home = home;
|
|
23
|
+
this.options = options;
|
|
24
|
+
}
|
|
25
|
+
get directory() { return path.join(this.home, 'repositories'); }
|
|
26
|
+
target(repository) {
|
|
27
|
+
const key = repository.toLowerCase();
|
|
28
|
+
return path.join(this.directory, key.split('/')[1].slice(0, 40) + '-' + createHash('sha256').update(key).digest('hex').slice(0, 16));
|
|
29
|
+
}
|
|
30
|
+
async git(args, signal, hooks) {
|
|
31
|
+
const env = this.options.env || process.env;
|
|
32
|
+
const result = await (this.options.execute || runProcess)('git', [
|
|
33
|
+
...(hooks ? ['-c', `core.hooksPath=${hooks}`] : []), '-c', 'submodule.recurse=false', ...args,
|
|
34
|
+
], { env: { ...env, GIT_TERMINAL_PROMPT: '0', GIT_SSH_COMMAND: env.GIT_SSH_COMMAND || 'ssh -oBatchMode=yes' }, signal, timeoutMs: 180000, maxBytes: 1024 * 1024 });
|
|
35
|
+
if (result.code !== 0)
|
|
36
|
+
throw new Error(`Could not open/update the repository. ${result.stderr.trim().slice(-1200) || 'Git exited with ' + result.code}. Private repositories require your existing Git credentials.`);
|
|
37
|
+
return result.stdout.trim();
|
|
38
|
+
}
|
|
39
|
+
async verify(target, repository, signal) {
|
|
40
|
+
const stat = await fs.lstat(target);
|
|
41
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
42
|
+
throw new Error('The saved repository path is not a regular directory.');
|
|
43
|
+
const root = await fs.realpath(target);
|
|
44
|
+
if (await fs.realpath(await this.git(['-C', root, 'rev-parse', '--show-toplevel'], signal)) !== root)
|
|
45
|
+
throw new Error('The saved checkout does not belong to this repository.');
|
|
46
|
+
const origin = await this.git(['-C', root, 'remote', 'get-url', 'origin'], signal);
|
|
47
|
+
if (repositoryFromRemote(origin)?.toLowerCase() !== repository.toLowerCase())
|
|
48
|
+
throw new Error('The saved checkout origin changed. Its files were preserved.');
|
|
49
|
+
return root;
|
|
50
|
+
}
|
|
51
|
+
async open(target, from = process.cwd(), signal, progress = () => { }) {
|
|
52
|
+
signal?.throwIfAborted();
|
|
53
|
+
if (!isRemoteSource(target)) {
|
|
54
|
+
const folder = target.replace(/^~(?=\/|$)/, os.homedir());
|
|
55
|
+
const root = await fs.realpath(path.resolve(from, folder));
|
|
56
|
+
if (!(await fs.stat(root)).isDirectory())
|
|
57
|
+
throw new Error('Choose a repository folder or GitHub URL.');
|
|
58
|
+
signal?.throwIfAborted();
|
|
59
|
+
return { root, reused: false };
|
|
60
|
+
}
|
|
61
|
+
const source = githubSource(target), destination = this.target(source.repository);
|
|
62
|
+
const existing = await fs.lstat(destination).then(() => true).catch(e => { if (e.code !== 'ENOENT')
|
|
63
|
+
throw e; return false; });
|
|
64
|
+
if (existing) {
|
|
65
|
+
const root = await this.verify(destination, source.repository, signal);
|
|
66
|
+
progress(`Using saved checkout of ${source.repository}. Use /pull to fetch the latest changes.`);
|
|
67
|
+
return { root, sourceUrl: source.url, reused: true };
|
|
68
|
+
}
|
|
69
|
+
await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
70
|
+
const staging = await fs.mkdtemp(path.join(this.directory, '.clone-'));
|
|
71
|
+
const hooks = path.join(staging, 'empty-hooks'), checkout = path.join(staging, 'checkout');
|
|
72
|
+
await fs.mkdir(hooks, { mode: 0o700 });
|
|
73
|
+
try {
|
|
74
|
+
progress(`Cloning ${source.repository}… Ctrl-C to stop.`);
|
|
75
|
+
await this.git(['clone', '--filter=blob:none', '--single-branch', '--', source.gitUrl, checkout], signal, hooks);
|
|
76
|
+
signal?.throwIfAborted();
|
|
77
|
+
await this.verify(checkout, source.repository, signal);
|
|
78
|
+
await fs.chmod(checkout, 0o700);
|
|
79
|
+
try {
|
|
80
|
+
await fs.rename(checkout, destination);
|
|
81
|
+
}
|
|
82
|
+
catch (e) {
|
|
83
|
+
// Two chats may clone the same URL concurrently. Reuse the complete
|
|
84
|
+
// winner, preserving it rather than replacing its existing contents.
|
|
85
|
+
if (!['EEXIST', 'ENOTEMPTY'].includes(e.code || ''))
|
|
86
|
+
throw e;
|
|
87
|
+
}
|
|
88
|
+
const root = await this.verify(destination, source.repository, signal);
|
|
89
|
+
progress(`Ready: ${source.repository}\nSaved checkout: ${root}\nAsk a question, or use /map to explore.`);
|
|
90
|
+
return { root, sourceUrl: source.url, reused: false };
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await fs.rm(staging, { recursive: true, force: true });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
async pull(root, signal, progress = () => { }) {
|
|
97
|
+
const origin = await this.git(['-C', root, 'remote', 'get-url', 'origin'], signal);
|
|
98
|
+
const repository = repositoryFromRemote(origin);
|
|
99
|
+
if (!repository || await fs.realpath(this.target(repository)).catch(() => null) !== root)
|
|
100
|
+
throw new Error('/pull updates a saved GitHub checkout. Open one with /repo <GitHub URL> first.');
|
|
101
|
+
await this.verify(root, repository, signal);
|
|
102
|
+
if (await this.git(['-C', root, 'status', '--porcelain', '--untracked-files=all'], signal))
|
|
103
|
+
throw new Error('The checkout has local changes. Preserve or commit them before /pull.');
|
|
104
|
+
const hooks = path.join(this.directory, '.pull-hooks-' + randomUUID());
|
|
105
|
+
await fs.mkdir(hooks, { mode: 0o700 });
|
|
106
|
+
try {
|
|
107
|
+
progress(`Pulling latest changes for ${repository}…`);
|
|
108
|
+
const result = await this.git(['-C', root, 'pull', '--ff-only', '--no-rebase'], signal, hooks);
|
|
109
|
+
return result || 'Repository is up to date.';
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
await fs.rm(hooks, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=source.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/cli/chat/source.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport os from 'node:os';\nimport { createHash, randomUUID } from 'node:crypto';\nimport { agentHome } from '../agent/config.js';\nimport { repositoryFromRemote } from '../agent/github.js';\nimport { runProcess } from '../agent/process.js';\n\nexport interface ChatSource { root: string; sourceUrl?: string; reused: boolean }\ninterface SourceOptions { env?: NodeJS.ProcessEnv; execute?: typeof runProcess }\nexport function isRemoteSource(value: string): boolean { return /^(?:https?:\\/\\/|ssh:\\/\\/|git@)/i.test(value); }\nexport function githubSource(value: string): { repository: string; url: string; gitUrl: string } {\n const raw = value.trim(), repository = repositoryFromRemote(raw);\n if (!repository) throw new Error('Enter a GitHub repository URL: https://github.com/owner/repo or git@github.com:owner/repo.git.');\n const url = `https://github.com/${repository}`;\n const gitUrl = /^https:/i.test(raw) ? url + '.git' : `git@github.com:${repository}.git`;\n return { repository, url, gitUrl };\n}\n\n/** Persistent checkouts keep conversations and agent worktrees usable after exit. */\nexport class ChatSources {\n constructor(readonly home = agentHome(), private options: SourceOptions = {}) {}\n get directory() { return path.join(this.home, 'repositories'); }\n private target(repository: string) {\n const key = repository.toLowerCase();\n return path.join(this.directory, key.split('/')[1]!.slice(0, 40) + '-' + createHash('sha256').update(key).digest('hex').slice(0, 16));\n }\n private async git(args: string[], signal?: AbortSignal, hooks?: string) {\n const env = this.options.env || process.env;\n const result = await (this.options.execute || runProcess)('git', [\n ...(hooks ? ['-c', `core.hooksPath=${hooks}`] : []), '-c', 'submodule.recurse=false', ...args,\n ], { env: { ...env, GIT_TERMINAL_PROMPT: '0', GIT_SSH_COMMAND: env.GIT_SSH_COMMAND || 'ssh -oBatchMode=yes' }, signal, timeoutMs: 180000, maxBytes: 1024 * 1024 });\n if (result.code !== 0) throw new Error(`Could not open/update the repository. ${result.stderr.trim().slice(-1200) || 'Git exited with ' + result.code}. Private repositories require your existing Git credentials.`);\n return result.stdout.trim();\n }\n private async verify(target: string, repository: string, signal?: AbortSignal) {\n const stat = await fs.lstat(target);\n if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error('The saved repository path is not a regular directory.');\n const root = await fs.realpath(target);\n if (await fs.realpath(await this.git(['-C', root, 'rev-parse', '--show-toplevel'], signal)) !== root) throw new Error('The saved checkout does not belong to this repository.');\n const origin = await this.git(['-C', root, 'remote', 'get-url', 'origin'], signal);\n if (repositoryFromRemote(origin)?.toLowerCase() !== repository.toLowerCase()) throw new Error('The saved checkout origin changed. Its files were preserved.');\n return root;\n }\n async open(target: string, from = process.cwd(), signal?: AbortSignal, progress: (text: string) => void = () => {}): Promise<ChatSource> {\n signal?.throwIfAborted();\n if (!isRemoteSource(target)) {\n const folder = target.replace(/^~(?=\\/|$)/, os.homedir());\n const root = await fs.realpath(path.resolve(from, folder));\n if (!(await fs.stat(root)).isDirectory()) throw new Error('Choose a repository folder or GitHub URL.');\n signal?.throwIfAborted();\n return { root, reused: false };\n }\n const source = githubSource(target), destination = this.target(source.repository);\n const existing = await fs.lstat(destination).then(() => true).catch(e => { if (e.code !== 'ENOENT') throw e; return false; });\n if (existing) {\n const root = await this.verify(destination, source.repository, signal);\n progress(`Using saved checkout of ${source.repository}. Use /pull to fetch the latest changes.`);\n return { root, sourceUrl: source.url, reused: true };\n }\n await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });\n const staging = await fs.mkdtemp(path.join(this.directory, '.clone-'));\n const hooks = path.join(staging, 'empty-hooks'), checkout = path.join(staging, 'checkout');\n await fs.mkdir(hooks, { mode: 0o700 });\n try {\n progress(`Cloning ${source.repository}… Ctrl-C to stop.`);\n await this.git(['clone', '--filter=blob:none', '--single-branch', '--', source.gitUrl, checkout], signal, hooks);\n signal?.throwIfAborted();\n await this.verify(checkout, source.repository, signal);\n await fs.chmod(checkout, 0o700);\n try { await fs.rename(checkout, destination); }\n catch (e) {\n // Two chats may clone the same URL concurrently. Reuse the complete\n // winner, preserving it rather than replacing its existing contents.\n if (!['EEXIST', 'ENOTEMPTY'].includes((e as NodeJS.ErrnoException).code || '')) throw e;\n }\n const root = await this.verify(destination, source.repository, signal);\n progress(`Ready: ${source.repository}\\nSaved checkout: ${root}\\nAsk a question, or use /map to explore.`);\n return { root, sourceUrl: source.url, reused: false };\n } finally { await fs.rm(staging, { recursive: true, force: true }); }\n }\n async pull(root: string, signal?: AbortSignal, progress: (text: string) => void = () => {}) {\n const origin = await this.git(['-C', root, 'remote', 'get-url', 'origin'], signal);\n const repository = repositoryFromRemote(origin);\n if (!repository || await fs.realpath(this.target(repository)).catch(() => null) !== root) throw new Error('/pull updates a saved GitHub checkout. Open one with /repo <GitHub URL> first.');\n await this.verify(root, repository, signal);\n if (await this.git(['-C', root, 'status', '--porcelain', '--untracked-files=all'], signal)) throw new Error('The checkout has local changes. Preserve or commit them before /pull.');\n const hooks = path.join(this.directory, '.pull-hooks-' + randomUUID());\n await fs.mkdir(hooks, { mode: 0o700 });\n try {\n progress(`Pulling latest changes for ${repository}…`);\n const result = await this.git(['-C', root, 'pull', '--ff-only', '--no-rebase'], signal, hooks);\n return result || 'Repository is up to date.';\n } finally { await fs.rm(hooks, { recursive: true, force: true }); }\n }\n}\n"]}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type AgentMode, type AgentResult } from '../agent/contracts.js';
|
|
2
|
+
export interface ChatSettings {
|
|
3
|
+
root: string;
|
|
4
|
+
sourceUrl?: string;
|
|
5
|
+
mode: AgentMode;
|
|
6
|
+
model?: string;
|
|
7
|
+
provider?: string;
|
|
8
|
+
checks: boolean;
|
|
9
|
+
github: boolean;
|
|
10
|
+
timeout: number;
|
|
11
|
+
maxTurns: number;
|
|
12
|
+
pr?: number;
|
|
13
|
+
issue?: number;
|
|
14
|
+
base?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ChatTurn {
|
|
17
|
+
at: string;
|
|
18
|
+
prompt: string;
|
|
19
|
+
answer: string;
|
|
20
|
+
run: string;
|
|
21
|
+
status: AgentResult['status'];
|
|
22
|
+
}
|
|
23
|
+
export interface ChatRecord {
|
|
24
|
+
schemaVersion: 1;
|
|
25
|
+
id: string;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
updatedAt: string;
|
|
28
|
+
settings: ChatSettings;
|
|
29
|
+
lastRun: string | null;
|
|
30
|
+
turns: ChatTurn[];
|
|
31
|
+
}
|
|
32
|
+
export declare function newChat(settings: ChatSettings): ChatRecord;
|
|
33
|
+
export declare class ChatStore {
|
|
34
|
+
readonly home: string;
|
|
35
|
+
private env;
|
|
36
|
+
constructor(home?: string, env?: NodeJS.ProcessEnv);
|
|
37
|
+
get directory(): string;
|
|
38
|
+
save(record: ChatRecord): Promise<void>;
|
|
39
|
+
load(id: string): Promise<ChatRecord>;
|
|
40
|
+
list(root: string): Promise<ChatRecord[]>;
|
|
41
|
+
export(record: ChatRecord): Promise<string>;
|
|
42
|
+
}
|
|
43
|
+
export declare function historySummary(records: ChatRecord[]): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { AGENT_MODES } from '../agent/contracts.js';
|
|
5
|
+
import { agentHome } from '../agent/config.js';
|
|
6
|
+
import { runId } from '../agent/runner.js';
|
|
7
|
+
import { redact, terminalText } from '../agent/process.js';
|
|
8
|
+
import { repositoryFromRemote } from '../agent/github.js';
|
|
9
|
+
export function newChat(settings) {
|
|
10
|
+
const now = new Date().toISOString();
|
|
11
|
+
return { schemaVersion: 1, id: randomUUID(), createdAt: now, updatedAt: now, settings: { ...settings }, lastRun: null, turns: [] };
|
|
12
|
+
}
|
|
13
|
+
function validSettings(s) {
|
|
14
|
+
return !!s && typeof s.root === 'string' && path.isAbsolute(s.root) && AGENT_MODES.includes(s.mode) &&
|
|
15
|
+
typeof s.checks === 'boolean' && typeof s.github === 'boolean' && Number.isInteger(s.timeout) && s.timeout >= 15 && s.timeout <= 3600 &&
|
|
16
|
+
Number.isInteger(s.maxTurns) && s.maxTurns >= 1 && s.maxTurns <= 100 &&
|
|
17
|
+
[s.model, s.provider, s.base].every(v => v === undefined || typeof v === 'string' && v.length <= 512) &&
|
|
18
|
+
[s.pr, s.issue].every(v => v === undefined || Number.isSafeInteger(v) && v > 0) &&
|
|
19
|
+
(s.sourceUrl === undefined || typeof s.sourceUrl === 'string' && repositoryFromRemote(s.sourceUrl) !== null);
|
|
20
|
+
}
|
|
21
|
+
export class ChatStore {
|
|
22
|
+
home;
|
|
23
|
+
env;
|
|
24
|
+
constructor(home = agentHome(), env = process.env) {
|
|
25
|
+
this.home = home;
|
|
26
|
+
this.env = env;
|
|
27
|
+
}
|
|
28
|
+
get directory() { return path.join(this.home, 'chats'); }
|
|
29
|
+
async save(record) {
|
|
30
|
+
await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });
|
|
31
|
+
const target = path.join(this.directory, runId(record.id) + '.json'), temp = target + '.' + randomUUID() + '.tmp';
|
|
32
|
+
const safe = { ...record, turns: record.turns.slice(-100).map(t => ({ ...t, prompt: redact(t.prompt, this.env), answer: redact(t.answer, this.env).slice(0, 32000) })) };
|
|
33
|
+
let encoded = JSON.stringify(safe, null, 2) + '\n';
|
|
34
|
+
// UTF-8 transcripts can use several bytes per character. Bound the stored
|
|
35
|
+
// bytes, rather than producing a conversation our own loader cannot read.
|
|
36
|
+
while (Buffer.byteLength(encoded) > 5 * 1024 * 1024 && safe.turns.length > 1) {
|
|
37
|
+
safe.turns.shift();
|
|
38
|
+
encoded = JSON.stringify(safe, null, 2) + '\n';
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
await fs.writeFile(temp, encoded, { mode: 0o600, flag: 'wx' });
|
|
42
|
+
await fs.rename(temp, target);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
await fs.rm(temp, { force: true });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async load(id) {
|
|
49
|
+
const file = path.join(this.directory, runId(id) + '.json');
|
|
50
|
+
if ((await fs.stat(file)).size > 6 * 1024 * 1024)
|
|
51
|
+
throw new Error('The saved conversation exceeds the size limit.');
|
|
52
|
+
const record = JSON.parse(await fs.readFile(file, 'utf8'));
|
|
53
|
+
if (record.schemaVersion !== 1 || record.id !== id || !validSettings(record.settings) ||
|
|
54
|
+
typeof record.createdAt !== 'string' || typeof record.updatedAt !== 'string' || !Array.isArray(record.turns) || record.turns.length > 100 ||
|
|
55
|
+
!(record.lastRun === null || typeof record.lastRun === 'string') || record.turns.some(t => !t || typeof t.at !== 'string' ||
|
|
56
|
+
typeof t.prompt !== 'string' || t.prompt.length > 16000 || typeof t.answer !== 'string' || t.answer.length > 32000 ||
|
|
57
|
+
typeof t.run !== 'string' || !['completed', 'failed', 'cancelled'].includes(t.status)))
|
|
58
|
+
throw new Error('The saved conversation is invalid.');
|
|
59
|
+
if (record.lastRun)
|
|
60
|
+
runId(record.lastRun);
|
|
61
|
+
for (const turn of record.turns)
|
|
62
|
+
runId(turn.run);
|
|
63
|
+
return record;
|
|
64
|
+
}
|
|
65
|
+
async list(root) {
|
|
66
|
+
const entries = await fs.readdir(this.directory, { withFileTypes: true }).catch(e => { if (e.code !== 'ENOENT')
|
|
67
|
+
throw e; return []; });
|
|
68
|
+
const records = [];
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
if (!entry.isFile() || !/^[a-f\d-]{36}\.json$/i.test(entry.name))
|
|
71
|
+
continue;
|
|
72
|
+
try {
|
|
73
|
+
const record = await this.load(entry.name.slice(0, -5));
|
|
74
|
+
if (record.settings.root === root)
|
|
75
|
+
records.push(record);
|
|
76
|
+
}
|
|
77
|
+
catch { /* A damaged entry must not hide the others. */ }
|
|
78
|
+
}
|
|
79
|
+
return records.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 30);
|
|
80
|
+
}
|
|
81
|
+
async export(record) {
|
|
82
|
+
const directory = path.join(this.home, 'exports');
|
|
83
|
+
await fs.mkdir(directory, { recursive: true, mode: 0o700 });
|
|
84
|
+
const file = path.join(directory, record.id + '-' + randomUUID().slice(0, 8) + '.md');
|
|
85
|
+
const text = ['# Onboarder conversation', '', `Repository: ${record.settings.sourceUrl || record.settings.root}`, `Conversation: ${record.id}`, '',
|
|
86
|
+
...record.turns.flatMap(turn => ['## You', '', turn.prompt, '', `## Hermes (${turn.status})`, '', turn.answer, '', `Run: ${turn.run}`, ''])].join('\n');
|
|
87
|
+
await fs.writeFile(file, redact(text, this.env), { flag: 'wx', mode: 0o600 });
|
|
88
|
+
return file;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
export function historySummary(records) {
|
|
92
|
+
return records.length ? records.map(r => `${r.id} ${r.settings.mode} ${r.turns.length} turns\n ${r.updatedAt} ${terminalText(r.turns[0]?.prompt || 'Empty conversation').replace(/\s+/g, ' ').slice(0, 100)}`).join('\n\n') : 'No saved conversations for this repository yet.';
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/cli/chat/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { promises as fs } from 'node:fs';\nimport path from 'node:path';\nimport { randomUUID } from 'node:crypto';\nimport { AGENT_MODES, type AgentMode, type AgentResult } from '../agent/contracts.js';\nimport { agentHome } from '../agent/config.js';\nimport { runId } from '../agent/runner.js';\nimport { redact, terminalText } from '../agent/process.js';\nimport { repositoryFromRemote } from '../agent/github.js';\n\nexport interface ChatSettings {\n root: string; sourceUrl?: string; mode: AgentMode; model?: string; provider?: string;\n checks: boolean; github: boolean; timeout: number; maxTurns: number; pr?: number; issue?: number; base?: string;\n}\nexport interface ChatTurn { at: string; prompt: string; answer: string; run: string; status: AgentResult['status'] }\nexport interface ChatRecord {\n schemaVersion: 1; id: string; createdAt: string; updatedAt: string;\n settings: ChatSettings; lastRun: string | null; turns: ChatTurn[];\n}\nexport function newChat(settings: ChatSettings): ChatRecord {\n const now = new Date().toISOString();\n return { schemaVersion: 1, id: randomUUID(), createdAt: now, updatedAt: now, settings: { ...settings }, lastRun: null, turns: [] };\n}\nfunction validSettings(s: ChatSettings): boolean {\n return !!s && typeof s.root === 'string' && path.isAbsolute(s.root) && AGENT_MODES.includes(s.mode) &&\n typeof s.checks === 'boolean' && typeof s.github === 'boolean' && Number.isInteger(s.timeout) && s.timeout >= 15 && s.timeout <= 3600 &&\n Number.isInteger(s.maxTurns) && s.maxTurns >= 1 && s.maxTurns <= 100 &&\n [s.model, s.provider, s.base].every(v => v === undefined || typeof v === 'string' && v.length <= 512) &&\n [s.pr, s.issue].every(v => v === undefined || Number.isSafeInteger(v) && v > 0) &&\n (s.sourceUrl === undefined || typeof s.sourceUrl === 'string' && repositoryFromRemote(s.sourceUrl) !== null);\n}\nexport class ChatStore {\n constructor(readonly home = agentHome(), private env: NodeJS.ProcessEnv = process.env) {}\n get directory() { return path.join(this.home, 'chats'); }\n async save(record: ChatRecord) {\n await fs.mkdir(this.directory, { recursive: true, mode: 0o700 });\n const target = path.join(this.directory, runId(record.id) + '.json'), temp = target + '.' + randomUUID() + '.tmp';\n const safe = { ...record, turns: record.turns.slice(-100).map(t => ({ ...t, prompt: redact(t.prompt, this.env), answer: redact(t.answer, this.env).slice(0, 32000) })) };\n let encoded = JSON.stringify(safe, null, 2) + '\\n';\n // UTF-8 transcripts can use several bytes per character. Bound the stored\n // bytes, rather than producing a conversation our own loader cannot read.\n while (Buffer.byteLength(encoded) > 5 * 1024 * 1024 && safe.turns.length > 1) {\n safe.turns.shift(); encoded = JSON.stringify(safe, null, 2) + '\\n';\n }\n try { await fs.writeFile(temp, encoded, { mode: 0o600, flag: 'wx' }); await fs.rename(temp, target); }\n finally { await fs.rm(temp, { force: true }); }\n }\n async load(id: string): Promise<ChatRecord> {\n const file = path.join(this.directory, runId(id) + '.json');\n if ((await fs.stat(file)).size > 6 * 1024 * 1024) throw new Error('The saved conversation exceeds the size limit.');\n const record = JSON.parse(await fs.readFile(file, 'utf8')) as ChatRecord;\n if (record.schemaVersion !== 1 || record.id !== id || !validSettings(record.settings) ||\n typeof record.createdAt !== 'string' || typeof record.updatedAt !== 'string' || !Array.isArray(record.turns) || record.turns.length > 100 ||\n !(record.lastRun === null || typeof record.lastRun === 'string') || record.turns.some(t => !t || typeof t.at !== 'string' ||\n typeof t.prompt !== 'string' || t.prompt.length > 16000 || typeof t.answer !== 'string' || t.answer.length > 32000 ||\n typeof t.run !== 'string' || !['completed', 'failed', 'cancelled'].includes(t.status))) throw new Error('The saved conversation is invalid.');\n if (record.lastRun) runId(record.lastRun);\n for (const turn of record.turns) runId(turn.run);\n return record;\n }\n async list(root: string): Promise<ChatRecord[]> {\n const entries = await fs.readdir(this.directory, { withFileTypes: true }).catch(e => { if (e.code !== 'ENOENT') throw e; return []; });\n const records: ChatRecord[] = [];\n for (const entry of entries) {\n if (!entry.isFile() || !/^[a-f\\d-]{36}\\.json$/i.test(entry.name)) continue;\n try { const record = await this.load(entry.name.slice(0, -5)); if (record.settings.root === root) records.push(record); } catch { /* A damaged entry must not hide the others. */ }\n }\n return records.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)).slice(0, 30);\n }\n async export(record: ChatRecord): Promise<string> {\n const directory = path.join(this.home, 'exports'); await fs.mkdir(directory, { recursive: true, mode: 0o700 });\n const file = path.join(directory, record.id + '-' + randomUUID().slice(0, 8) + '.md');\n const text = ['# Onboarder conversation', '', `Repository: ${record.settings.sourceUrl || record.settings.root}`, `Conversation: ${record.id}`, '',\n ...record.turns.flatMap(turn => ['## You', '', turn.prompt, '', `## Hermes (${turn.status})`, '', turn.answer, '', `Run: ${turn.run}`, ''])].join('\\n');\n await fs.writeFile(file, redact(text, this.env), { flag: 'wx', mode: 0o600 }); return file;\n }\n}\nexport function historySummary(records: ChatRecord[]): string {\n return records.length ? records.map(r => `${r.id} ${r.settings.mode} ${r.turns.length} turns\\n ${r.updatedAt} ${terminalText(r.turns[0]?.prompt || 'Empty conversation').replace(/\\s+/g, ' ').slice(0, 100)}`).join('\\n\\n') : 'No saved conversations for this repository yet.';\n}\n"]}
|
package/cli/chat/text.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { terminalText } from '../agent/process.js';
|
|
2
|
+
const segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
|
|
3
|
+
export function graphemes(value) { return [...segmenter.segment(value)].map(s => s.segment); }
|
|
4
|
+
export function clusterCells(cluster) {
|
|
5
|
+
if (/^[\p{Mark}\u200d\ufe0f]+$/u.test(cluster))
|
|
6
|
+
return 0;
|
|
7
|
+
return /\p{Extended_Pictographic}|[\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff01-\uff60\uffe0-\uffe6\u{1f1e6}-\u{1f1ff}]/u.test(cluster) ? 2 : 1;
|
|
8
|
+
}
|
|
9
|
+
export function cells(value) {
|
|
10
|
+
return graphemes(terminalText(value)).reduce((total, cluster) => total + clusterCells(cluster), 0);
|
|
11
|
+
}
|
|
12
|
+
export function clip(value, room, ellipsis = true) {
|
|
13
|
+
const safe = terminalText(value).replace(/[\r\n\t]/g, ' ');
|
|
14
|
+
if (cells(safe) <= room)
|
|
15
|
+
return safe;
|
|
16
|
+
const limit = Math.max(0, room - (ellipsis && room > 0 ? 1 : 0));
|
|
17
|
+
let result = '', used = 0;
|
|
18
|
+
for (const g of graphemes(safe)) {
|
|
19
|
+
const size = clusterCells(g);
|
|
20
|
+
if (used + size > limit)
|
|
21
|
+
break;
|
|
22
|
+
result += g;
|
|
23
|
+
used += size;
|
|
24
|
+
}
|
|
25
|
+
return result + (ellipsis && room > 0 ? '…' : '');
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../../src/cli/chat/text.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AAEnD,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC,CAAC;AAC7E,MAAM,UAAU,SAAS,CAAC,KAAa,IAAc,OAAO,CAAC,GAAG,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;AAChH,MAAM,UAAU,YAAY,CAAC,OAAe;IAC1C,IAAI,4BAA4B,CAAC,IAAI,CAAC,OAAO,CAAC;QAAE,OAAO,CAAC,CAAC;IACzD,OAAO,sKAAsK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtM,CAAC;AACD,MAAM,UAAU,KAAK,CAAC,KAAa;IACjC,OAAO,SAAS,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,OAAO,EAAE,EAAE,CAAC,KAAK,GAAG,YAAY,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC;AACrG,CAAC;AACD,MAAM,UAAU,IAAI,CAAC,KAAa,EAAE,IAAY,EAAE,QAAQ,GAAG,IAAI;IAC/D,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,GAAG,CAAC,CAAC;IAC3D,IAAI,KAAK,CAAC,IAAI,CAAC,IAAI,IAAI;QAAE,OAAO,IAAI,CAAC;IACrC,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,CAAC,QAAQ,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACjE,IAAI,MAAM,GAAG,EAAE,EAAE,IAAI,GAAG,CAAC,CAAC;IAC1B,KAAK,MAAM,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC;QAAC,MAAM,IAAI,GAAG,YAAY,CAAC,CAAC,CAAC,CAAC;QAAC,IAAI,IAAI,GAAG,IAAI,GAAG,KAAK;YAAE,MAAM;QAAC,MAAM,IAAI,CAAC,CAAC;QAAC,IAAI,IAAI,IAAI,CAAC;IAAC,CAAC;IAC7H,OAAO,MAAM,GAAG,CAAC,QAAQ,IAAI,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACpD,CAAC","sourcesContent":["import { terminalText } from '../agent/process.js';\n\nconst segmenter = new Intl.Segmenter(undefined, { granularity: 'grapheme' });\nexport function graphemes(value: string): string[] { return [...segmenter.segment(value)].map(s => s.segment); }\nexport function clusterCells(cluster: string): number {\n if (/^[\\p{Mark}\\u200d\\ufe0f]+$/u.test(cluster)) return 0;\n return /\\p{Extended_Pictographic}|[\\u1100-\\u115f\\u2329\\u232a\\u2e80-\\ua4cf\\uac00-\\ud7a3\\uf900-\\ufaff\\ufe10-\\ufe19\\ufe30-\\ufe6f\\uff01-\\uff60\\uffe0-\\uffe6\\u{1f1e6}-\\u{1f1ff}]/u.test(cluster) ? 2 : 1;\n}\nexport function cells(value: string): number {\n return graphemes(terminalText(value)).reduce((total, cluster) => total + clusterCells(cluster), 0);\n}\nexport function clip(value: string, room: number, ellipsis = true): string {\n const safe = terminalText(value).replace(/[\\r\\n\\t]/g, ' ');\n if (cells(safe) <= room) return safe;\n const limit = Math.max(0, room - (ellipsis && room > 0 ? 1 : 0));\n let result = '', used = 0;\n for (const g of graphemes(safe)) { const size = clusterCells(g); if (used + size > limit) break; result += g; used += size; }\n return result + (ellipsis && room > 0 ? '…' : '');\n}\n"]}
|
package/cli/explorer/app.d.ts
CHANGED
package/cli/explorer/app.js
CHANGED
|
@@ -16,6 +16,7 @@ import { scanOptionsFromFlags } from '../scanOptions.js';
|
|
|
16
16
|
// * Flags → env → config precedence. `NO_COLOR` and `COLUMNS` are honored
|
|
17
17
|
// before anything is drawn, so a redirect gets plain, fitted text.
|
|
18
18
|
import readline from 'node:readline';
|
|
19
|
+
import { explorerAgent } from '../agent/command.js';
|
|
19
20
|
import readlinePromises from 'node:readline/promises';
|
|
20
21
|
import fs from 'node:fs';
|
|
21
22
|
import path from 'node:path';
|
|
@@ -140,6 +141,7 @@ async function shouldShowHint(flags) {
|
|
|
140
141
|
// interleaved `cd`s would leave the session pointing at a repo nobody asked
|
|
141
142
|
// for.
|
|
142
143
|
function session({ repo, flags, out, err, version }) {
|
|
144
|
+
let agentAbort = null;
|
|
143
145
|
const ctx = {
|
|
144
146
|
picker: null,
|
|
145
147
|
repo,
|
|
@@ -153,6 +155,17 @@ function session({ repo, flags, out, err, version }) {
|
|
|
153
155
|
return ctx.picker.view();
|
|
154
156
|
},
|
|
155
157
|
github: () => askGithub(ctx),
|
|
158
|
+
agent: async (args) => {
|
|
159
|
+
if (ctx.repo.cloneDir && ['implement', 'pr'].includes(args[0]))
|
|
160
|
+
throw new Error('Open a persistent local checkout before implementing changes or creating a PR. This explorer clone is removed when the session ends.');
|
|
161
|
+
agentAbort = new AbortController();
|
|
162
|
+
try {
|
|
163
|
+
return await explorerAgent(args, ctx.repo.root, agentAbort.signal, out, err);
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
agentAbort = null;
|
|
167
|
+
}
|
|
168
|
+
},
|
|
156
169
|
web: () => startWeb(ctx),
|
|
157
170
|
diff: async (base = 'HEAD', head = '', file = '') => formatDiff(await getGitDiff(ctx.repo.root, { base, head, file }), ctx.repo.facts.importers || {}, ctx.repo.scan.files.length, file),
|
|
158
171
|
engines: () => formatEngines(toolsStatus()),
|
|
@@ -226,6 +239,7 @@ function session({ repo, flags, out, err, version }) {
|
|
|
226
239
|
// its input is followed immediately by `exit`. The exit waits on the queue.
|
|
227
240
|
rl.on('close', () => {
|
|
228
241
|
closed = true;
|
|
242
|
+
agentAbort?.abort();
|
|
229
243
|
// The listener is removed on every exit path, including the interval one
|
|
230
244
|
// below, so a session that ends by any route leaves stdout as it found it.
|
|
231
245
|
if (onResize)
|
|
@@ -236,6 +250,11 @@ function session({ repo, flags, out, err, version }) {
|
|
|
236
250
|
rl.on('SIGINT', () => {
|
|
237
251
|
if (closed)
|
|
238
252
|
return;
|
|
253
|
+
if (agentAbort) {
|
|
254
|
+
agentAbort.abort();
|
|
255
|
+
out(dim('\n Cancelling agent…'));
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
239
258
|
if (ctx.picker) {
|
|
240
259
|
ctx.picker = null;
|
|
241
260
|
out(dim('\n Selection cancelled.'));
|
|
@@ -403,7 +422,7 @@ async function askGithub(ctx) {
|
|
|
403
422
|
// prints where it is; if not, it starts it detached, so the person keeps their
|
|
404
423
|
// session. The URL is the same one `onboarder start` would print, because it
|
|
405
424
|
// comes from the same `serverUrls` the CLI and the banner use.
|
|
406
|
-
async function startWeb(ctx) {
|
|
425
|
+
export async function startWeb(ctx) {
|
|
407
426
|
const file = ctx.flags.config || configPath();
|
|
408
427
|
const settings = await readSettings(file).catch(() => null);
|
|
409
428
|
const urls = serverUrls(settings || undefined);
|