@minko-ai/minko 0.1.0 → 0.1.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 +577 -2
- package/assets/mascot-preview.png +0 -0
- package/assets/mascot-preview.txt +46 -0
- package/assets/mascot-source.png +0 -0
- package/assets/skills/README.md +21 -0
- package/assets/skills/brainstorming/LICENSE +21 -0
- package/assets/skills/brainstorming/SKILL.md +316 -0
- package/assets/skills/buddhist-method/LICENSE +21 -0
- package/assets/skills/buddhist-method/SKILL.md +137 -0
- package/assets/skills/buddhist-method/references/ariyasacca-debug.md +57 -0
- package/assets/skills/buddhist-method/references/extended-principles.md +100 -0
- package/assets/skills/code-humanizer/LICENSE +21 -0
- package/assets/skills/code-humanizer/SKILL.md +174 -0
- package/assets/skills/code-review/LICENSE +21 -0
- package/assets/skills/code-review/SKILL.md +232 -0
- package/assets/skills/code-review/reference/angular.md +768 -0
- package/assets/skills/code-review/reference/architecture-review-guide.md +472 -0
- package/assets/skills/code-review/reference/c.md +890 -0
- package/assets/skills/code-review/reference/code-quality-universal.md +488 -0
- package/assets/skills/code-review/reference/code-review-best-practices.md +136 -0
- package/assets/skills/code-review/reference/common-bugs-checklist.md +302 -0
- package/assets/skills/code-review/reference/cpp.md +893 -0
- package/assets/skills/code-review/reference/cross-cutting/async-concurrency-patterns.md +515 -0
- package/assets/skills/code-review/reference/cross-cutting/error-handling-principles.md +492 -0
- package/assets/skills/code-review/reference/cross-cutting/n-plus-one-queries.md +309 -0
- package/assets/skills/code-review/reference/cross-cutting/sql-injection-prevention.md +308 -0
- package/assets/skills/code-review/reference/cross-cutting/xss-prevention.md +264 -0
- package/assets/skills/code-review/reference/csharp.md +525 -0
- package/assets/skills/code-review/reference/css-less-sass.md +661 -0
- package/assets/skills/code-review/reference/dart.md +670 -0
- package/assets/skills/code-review/reference/django.md +985 -0
- package/assets/skills/code-review/reference/fastapi.md +580 -0
- package/assets/skills/code-review/reference/go.md +993 -0
- package/assets/skills/code-review/reference/java.md +409 -0
- package/assets/skills/code-review/reference/java8.md +586 -0
- package/assets/skills/code-review/reference/kotlin.md +1018 -0
- package/assets/skills/code-review/reference/nestjs.md +593 -0
- package/assets/skills/code-review/reference/performance-review-guide.md +816 -0
- package/assets/skills/code-review/reference/php.md +684 -0
- package/assets/skills/code-review/reference/python.md +1073 -0
- package/assets/skills/code-review/reference/qt.md +757 -0
- package/assets/skills/code-review/reference/react.md +871 -0
- package/assets/skills/code-review/reference/ruby.md +964 -0
- package/assets/skills/code-review/reference/rust.md +846 -0
- package/assets/skills/code-review/reference/security-review-guide.md +494 -0
- package/assets/skills/code-review/reference/svelte.md +1064 -0
- package/assets/skills/code-review/reference/swift.md +936 -0
- package/assets/skills/code-review/reference/typescript.md +1016 -0
- package/assets/skills/code-review/reference/vue.md +924 -0
- package/assets/skills/code-review/reference/zig.md +440 -0
- package/assets/skills/cyclomatic-complexity/LICENSE +201 -0
- package/assets/skills/cyclomatic-complexity/SKILL.md +69 -0
- package/assets/skills/five-whys/LICENSE +21 -0
- package/assets/skills/five-whys/SKILL.md +247 -0
- package/assets/skills/five-whys/examples/performance-regression.md +161 -0
- package/assets/skills/five-whys/examples/production-incident.md +134 -0
- package/assets/skills/five-whys/references/software-patterns.md +284 -0
- package/assets/skills/five-whys/references/toyota-origins.md +257 -0
- package/assets/skills/systematic-debugging/LICENSE +21 -0
- package/assets/skills/systematic-debugging/SKILL.md +283 -0
- package/assets/skills/test-driven-development/LICENSE +21 -0
- package/assets/skills/test-driven-development/SKILL.md +330 -0
- package/assets/skills/verification-before-completion/LICENSE +21 -0
- package/assets/skills/verification-before-completion/SKILL.md +120 -0
- package/assets/skills/vibe-sec/LICENSE +201 -0
- package/assets/skills/vibe-sec/SKILL.md +758 -0
- package/assets/skills/writing-plans/LICENSE +21 -0
- package/assets/skills/writing-plans/SKILL.md +204 -0
- package/bin/minko.js +2 -2
- package/dist/agent/approvalTokens.js +224 -0
- package/dist/agent/branchLock.js +315 -0
- package/dist/agent/compact.js +182 -0
- package/dist/agent/context.js +183 -0
- package/dist/agent/cron.js +326 -0
- package/dist/agent/hooks.js +211 -0
- package/dist/agent/lanes.js +511 -0
- package/dist/agent/loop.js +337 -0
- package/dist/agent/memory.js +159 -0
- package/dist/agent/permissions.js +71 -0
- package/dist/agent/plan.js +118 -0
- package/dist/agent/prompt.js +120 -0
- package/dist/agent/subagent.js +67 -0
- package/dist/agent/tasks.js +449 -0
- package/dist/agent/transcript.js +62 -0
- package/dist/agent/workerStore.js +172 -0
- package/dist/agent/workerTypes.js +1 -0
- package/dist/agent/workers.js +503 -0
- package/dist/cli/app.js +261 -0
- package/dist/cli/argv.js +206 -0
- package/dist/cli/commandsCommand.js +12 -0
- package/dist/cli/configCommand.js +170 -0
- package/dist/cli/doctorCommand.js +112 -0
- package/dist/cli/exportCommand.js +85 -0
- package/dist/cli/forkCommand.js +24 -0
- package/dist/cli/linkCommand.js +272 -0
- package/dist/cli/loginCommand.js +69 -0
- package/dist/cli/mcpLoginCommand.js +68 -0
- package/dist/cli/pluginCommand.js +113 -0
- package/dist/cli/powerCommand.js +174 -0
- package/dist/cli/promptFooter.js +74 -0
- package/dist/cli/relayCommand.js +38 -0
- package/dist/cli/sandboxCommand.js +31 -0
- package/dist/cli/serveCommand.js +104 -0
- package/dist/cli/statusCommand.js +120 -0
- package/dist/cli/tui.js +1104 -0
- package/dist/commands/registry.js +99 -0
- package/dist/config/paths.js +81 -0
- package/dist/config/store.js +605 -0
- package/dist/index.js +199 -0
- package/dist/link/protocol.js +159 -0
- package/dist/link/relay.js +184 -0
- package/dist/link/server.js +566 -0
- package/dist/link/tunnel.js +90 -0
- package/dist/lsp/client.js +340 -0
- package/dist/lsp/tool.js +177 -0
- package/dist/mcp/client.js +469 -0
- package/dist/mcp/httpClient.js +191 -0
- package/dist/mcp/server.js +206 -0
- package/dist/mcp/tools.js +173 -0
- package/dist/oauth/device.js +111 -0
- package/dist/oauth/store.js +56 -0
- package/dist/plugins/loader.js +291 -0
- package/dist/plugins/market.js +98 -0
- package/dist/providers/anthropic.js +279 -0
- package/dist/providers/factory.js +109 -0
- package/dist/providers/openai.js +182 -0
- package/dist/providers/retry.js +95 -0
- package/dist/providers/sse.js +67 -0
- package/dist/providers/types.js +10 -0
- package/dist/rag/embeddings.js +171 -0
- package/dist/rag/index.js +373 -0
- package/dist/rag/server.js +167 -0
- package/dist/rag/tool.js +148 -0
- package/dist/sandbox/index.js +55 -0
- package/dist/sdk/server.js +150 -0
- package/dist/sdk/session.js +104 -0
- package/dist/session/liveness.js +148 -0
- package/dist/session/recorder.js +49 -0
- package/dist/session/store.js +171 -0
- package/dist/skills/index.js +428 -0
- package/dist/theme.js +30 -0
- package/dist/tools/fs.js +195 -0
- package/dist/tools/notebook.js +109 -0
- package/dist/tools/registry.js +32 -0
- package/dist/tools/search.js +130 -0
- package/dist/tools/shell.js +200 -0
- package/dist/tools/structured.js +83 -0
- package/dist/tools/types.js +1 -0
- package/dist/ui/ansi.js +135 -0
- package/dist/ui/banner.js +103 -0
- package/dist/ui/input.js +49 -0
- package/dist/ui/inputModel.js +113 -0
- package/dist/ui/keyboard.js +142 -0
- package/dist/ui/mascot.js +34 -0
- package/dist/ui/mascotData.js +96 -0
- package/dist/ui/md.js +183 -0
- package/dist/ui/screen.js +49 -0
- package/dist/ui/spinner.js +52 -0
- package/dist/ui/transcript.js +287 -0
- package/dist/util/clipboard.js +53 -0
- package/dist/util/errors.js +50 -0
- package/dist/util/version.js +12 -0
- package/dist/web/fetch.js +168 -0
- package/dist/web/search.js +97 -0
- package/package.json +45 -36
- package/src/agent/loop.ts +0 -159
- package/src/agent/prompt.ts +0 -45
- package/src/cli/app.ts +0 -396
- package/src/cli/argv.ts +0 -139
- package/src/cli/configCommand.ts +0 -175
- package/src/config/paths.ts +0 -20
- package/src/config/store.ts +0 -149
- package/src/index.ts +0 -120
- package/src/providers/anthropic.ts +0 -215
- package/src/providers/openai.ts +0 -158
- package/src/providers/sse.ts +0 -57
- package/src/providers/types.ts +0 -59
- package/src/session/store.ts +0 -125
- package/src/theme.ts +0 -41
- package/src/tools/fs.ts +0 -143
- package/src/tools/registry.ts +0 -19
- package/src/tools/shell.ts +0 -120
- package/src/tools/types.ts +0 -26
- package/src/ui/ansi.ts +0 -110
- package/src/ui/banner.ts +0 -39
- package/src/ui/input.ts +0 -54
- package/src/ui/md.ts +0 -160
- package/src/ui/spinner.ts +0 -58
- package/src/util/errors.ts +0 -48
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
const DEFAULT_IMAGE = 'debian:bookworm-slim';
|
|
4
|
+
export function resolveSandbox(config) {
|
|
5
|
+
const sandbox = config?.sandbox;
|
|
6
|
+
if (!sandbox || sandbox.mode !== 'docker') {
|
|
7
|
+
return null;
|
|
8
|
+
}
|
|
9
|
+
return {
|
|
10
|
+
mode: 'docker',
|
|
11
|
+
image: sandbox.image && sandbox.image.length > 0 ? sandbox.image : DEFAULT_IMAGE,
|
|
12
|
+
network: sandbox.network === true,
|
|
13
|
+
extraArgs: sandbox.extraArgs ?? [],
|
|
14
|
+
allowFallback: sandbox.allowFallback === true,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** Builds `docker run` arguments with the workspace mounted at /workspace. */
|
|
18
|
+
export function dockerArgs(options, command, cwd) {
|
|
19
|
+
const workspace = path.resolve(cwd);
|
|
20
|
+
return [
|
|
21
|
+
'run',
|
|
22
|
+
'--rm',
|
|
23
|
+
'-i',
|
|
24
|
+
'-v', workspace + ':/workspace',
|
|
25
|
+
'-w', '/workspace',
|
|
26
|
+
...(options.network ? [] : ['--network', 'none']),
|
|
27
|
+
...options.extraArgs,
|
|
28
|
+
options.image,
|
|
29
|
+
'sh',
|
|
30
|
+
'-lc',
|
|
31
|
+
command,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
let dockerProbe = null;
|
|
35
|
+
export function dockerAvailable() {
|
|
36
|
+
if (dockerProbe !== null) {
|
|
37
|
+
return dockerProbe;
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
const result = spawnSync('docker', ['version', '--format', '{{.Server.Version}}'], {
|
|
41
|
+
timeout: 3_000,
|
|
42
|
+
windowsHide: true,
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
});
|
|
45
|
+
dockerProbe = result.status === 0;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
dockerProbe = false;
|
|
49
|
+
}
|
|
50
|
+
return dockerProbe;
|
|
51
|
+
}
|
|
52
|
+
/** Test seam for environments where Docker availability changes. */
|
|
53
|
+
export function resetDockerProbeForTests() {
|
|
54
|
+
dockerProbe = null;
|
|
55
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import readline from 'node:readline';
|
|
2
|
+
import { SessionStore } from "../session/store.js";
|
|
3
|
+
import { ApprovalTokenStore } from "../agent/approvalTokens.js";
|
|
4
|
+
import { readVersion } from "../util/version.js";
|
|
5
|
+
import { SdkSession } from "./session.js";
|
|
6
|
+
/** NDJSON JSON-RPC server for programmatic Minko sessions. */
|
|
7
|
+
export class SdkServer {
|
|
8
|
+
store;
|
|
9
|
+
cwd;
|
|
10
|
+
config;
|
|
11
|
+
getProvider;
|
|
12
|
+
approvalStore;
|
|
13
|
+
onEvent;
|
|
14
|
+
busy = new Set();
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.store = options.store ?? new SessionStore();
|
|
17
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
18
|
+
this.config = options.config;
|
|
19
|
+
this.getProvider = options.getProvider;
|
|
20
|
+
this.approvalStore = options.approvalStore ?? new ApprovalTokenStore();
|
|
21
|
+
this.onEvent = options.onEvent ?? (() => { });
|
|
22
|
+
}
|
|
23
|
+
async handle(message) {
|
|
24
|
+
const method = typeof message.method === 'string' ? message.method : '';
|
|
25
|
+
const id = message.id;
|
|
26
|
+
const params = message.params !== null && typeof message.params === 'object'
|
|
27
|
+
? message.params : {};
|
|
28
|
+
if (method === 'initialize') {
|
|
29
|
+
return result(id, {
|
|
30
|
+
protocolVersion: '1',
|
|
31
|
+
serverInfo: { name: 'minko-sdk', version: readVersion() },
|
|
32
|
+
capabilities: { chat: true, streaming: true, sessions: true },
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (method === 'session.create') {
|
|
36
|
+
const cwd = typeof params.cwd === 'string' && params.cwd.trim().length > 0
|
|
37
|
+
? params.cwd.trim() : this.cwd;
|
|
38
|
+
const model = typeof params.model === 'string' && params.model.trim().length > 0
|
|
39
|
+
? params.model.trim() : 'sdk';
|
|
40
|
+
const meta = this.store.create(cwd, model);
|
|
41
|
+
return result(id, { session: meta });
|
|
42
|
+
}
|
|
43
|
+
if (method === 'session.list') {
|
|
44
|
+
return result(id, { sessions: this.store.list() });
|
|
45
|
+
}
|
|
46
|
+
if (method === 'session.get') {
|
|
47
|
+
const sessionId = typeof params.session === 'string' ? params.session : '';
|
|
48
|
+
const record = this.store.load(sessionId);
|
|
49
|
+
if (!record) {
|
|
50
|
+
return error(id, -32602, 'session not found: ' + sessionId);
|
|
51
|
+
}
|
|
52
|
+
return result(id, record);
|
|
53
|
+
}
|
|
54
|
+
if (method === 'session.delete') {
|
|
55
|
+
const sessionId = typeof params.session === 'string' ? params.session : '';
|
|
56
|
+
return result(id, { ok: this.store.delete(sessionId) });
|
|
57
|
+
}
|
|
58
|
+
if (method === 'chat') {
|
|
59
|
+
const sessionId = typeof params.session === 'string' ? params.session : '';
|
|
60
|
+
const text = typeof params.text === 'string' ? params.text : '';
|
|
61
|
+
if (text.trim().length === 0) {
|
|
62
|
+
return error(id, -32602, 'text is required');
|
|
63
|
+
}
|
|
64
|
+
if (this.busy.has(sessionId)) {
|
|
65
|
+
return error(id, -32603, 'session is busy');
|
|
66
|
+
}
|
|
67
|
+
const record = this.store.load(sessionId);
|
|
68
|
+
if (!record) {
|
|
69
|
+
return error(id, -32602, 'session not found: ' + sessionId);
|
|
70
|
+
}
|
|
71
|
+
this.busy.add(sessionId);
|
|
72
|
+
try {
|
|
73
|
+
const session = new SdkSession({
|
|
74
|
+
store: this.store,
|
|
75
|
+
record,
|
|
76
|
+
provider: this.getProvider(),
|
|
77
|
+
config: this.config,
|
|
78
|
+
approval: params.approval === 'auto' ? 'auto' : 'ask',
|
|
79
|
+
approvalStore: this.approvalStore,
|
|
80
|
+
onEvent: (event) => this.onEvent(id ?? null, event),
|
|
81
|
+
});
|
|
82
|
+
const outcome = await session.chat(text);
|
|
83
|
+
return result(id, {
|
|
84
|
+
text: outcome.text,
|
|
85
|
+
usage: outcome.usage,
|
|
86
|
+
messages: outcome.messages,
|
|
87
|
+
session: record.meta,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (caught) {
|
|
91
|
+
return error(id, -32603, caught instanceof Error ? caught.message : String(caught));
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
this.busy.delete(sessionId);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (method === 'shutdown') {
|
|
98
|
+
return result(id, null);
|
|
99
|
+
}
|
|
100
|
+
if (method === 'exit') {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (id !== undefined && id !== null) {
|
|
104
|
+
return error(id, -32601, 'method not found: ' + method);
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export async function runSdkServer(options) {
|
|
110
|
+
const server = new SdkServer({
|
|
111
|
+
...options,
|
|
112
|
+
onEvent: (requestId, event) => {
|
|
113
|
+
const payload = JSON.stringify({
|
|
114
|
+
jsonrpc: '2.0',
|
|
115
|
+
method: 'minko.event',
|
|
116
|
+
params: { requestId, event },
|
|
117
|
+
});
|
|
118
|
+
process.stdout.write(payload + '\n');
|
|
119
|
+
options.onEvent?.(requestId, event);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
process.stderr.write('Minko SDK 已启动(NDJSON JSON-RPC,stdout 只输出协议消息)\n');
|
|
123
|
+
const rl = readline.createInterface({ input: process.stdin });
|
|
124
|
+
for await (const line of rl) {
|
|
125
|
+
if (line.trim().length === 0) {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
let message;
|
|
129
|
+
try {
|
|
130
|
+
message = JSON.parse(line);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const response = await server.handle(message);
|
|
136
|
+
if (response) {
|
|
137
|
+
process.stdout.write(JSON.stringify(response) + '\n');
|
|
138
|
+
}
|
|
139
|
+
if (message.method === 'shutdown' || message.method === 'exit') {
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
145
|
+
function result(id, value) {
|
|
146
|
+
return { jsonrpc: '2.0', id, result: value };
|
|
147
|
+
}
|
|
148
|
+
function error(id, code, message) {
|
|
149
|
+
return { jsonrpc: '2.0', id, error: { code, message } };
|
|
150
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { runAgent } from "../agent/loop.js";
|
|
2
|
+
import { createPlanTools } from "../agent/plan.js";
|
|
3
|
+
import { createMemoryTools, defaultMemoryDir, loadMemoryContext, } from "../agent/memory.js";
|
|
4
|
+
import { ApprovalTokenStore } from "../agent/approvalTokens.js";
|
|
5
|
+
import { loadProjectContext } from "../agent/context.js";
|
|
6
|
+
import { systemPrompt } from "../agent/prompt.js";
|
|
7
|
+
import { ALL_TOOLS } from "../tools/registry.js";
|
|
8
|
+
import { titleFromMessages } from "../session/store.js";
|
|
9
|
+
import { TurnRecorder } from "../session/recorder.js";
|
|
10
|
+
/** One persistent Minko session driven programmatically. */
|
|
11
|
+
export class SdkSession {
|
|
12
|
+
store;
|
|
13
|
+
record;
|
|
14
|
+
provider;
|
|
15
|
+
config;
|
|
16
|
+
approval;
|
|
17
|
+
approve;
|
|
18
|
+
approvalStore;
|
|
19
|
+
onEvent;
|
|
20
|
+
constructor(options) {
|
|
21
|
+
this.store = options.store;
|
|
22
|
+
this.record = options.record;
|
|
23
|
+
this.provider = options.provider;
|
|
24
|
+
this.config = options.config;
|
|
25
|
+
this.approval = options.approval ?? 'ask';
|
|
26
|
+
this.approve = options.approve ?? (async () => false);
|
|
27
|
+
this.approvalStore = options.approvalStore ?? new ApprovalTokenStore();
|
|
28
|
+
this.onEvent = options.onEvent ?? (() => { });
|
|
29
|
+
}
|
|
30
|
+
async chat(text, signal = new AbortController().signal) {
|
|
31
|
+
const prompt = text.trim();
|
|
32
|
+
if (prompt.length === 0) {
|
|
33
|
+
throw new Error('text is required');
|
|
34
|
+
}
|
|
35
|
+
const history = this.record.messages;
|
|
36
|
+
history.push({ role: 'user', content: prompt });
|
|
37
|
+
this.record.meta.title = titleFromMessages(history);
|
|
38
|
+
this.store.save(this.record.meta, history);
|
|
39
|
+
const recorder = new TurnRecorder(this.store, this.record.meta, history);
|
|
40
|
+
const cwd = this.record.meta.cwd;
|
|
41
|
+
const system = systemPrompt({
|
|
42
|
+
cwd,
|
|
43
|
+
project: loadProjectContext(cwd),
|
|
44
|
+
planMode: this.record.meta.planMode,
|
|
45
|
+
memory: loadMemoryContext(defaultMemoryDir()),
|
|
46
|
+
});
|
|
47
|
+
const tools = [
|
|
48
|
+
...ALL_TOOLS,
|
|
49
|
+
...createPlanTools(this.record.meta),
|
|
50
|
+
...createMemoryTools(defaultMemoryDir()),
|
|
51
|
+
];
|
|
52
|
+
try {
|
|
53
|
+
const outcome = await runAgent({
|
|
54
|
+
provider: this.provider,
|
|
55
|
+
tools,
|
|
56
|
+
messages: [{ role: 'system', content: system }, ...history],
|
|
57
|
+
cwd,
|
|
58
|
+
signal,
|
|
59
|
+
approval: this.approval,
|
|
60
|
+
power: this.config?.power ?? 'restricted',
|
|
61
|
+
permissions: this.config?.permissions,
|
|
62
|
+
toolHooks: this.config?.hooks,
|
|
63
|
+
planState: this.record.meta,
|
|
64
|
+
config: this.config,
|
|
65
|
+
approvalStore: this.approvalStore,
|
|
66
|
+
maxRounds: 40,
|
|
67
|
+
hooks: {
|
|
68
|
+
onText: (delta) => this.onEvent({ type: 'text', text: delta }),
|
|
69
|
+
onReasoning: (delta) => this.onEvent({ type: 'reasoning', text: delta }),
|
|
70
|
+
onToolStart: (call, args) => this.onEvent({ type: 'tool_start', call, args }),
|
|
71
|
+
onToolEnd: (call, result) => this.onEvent({
|
|
72
|
+
type: 'tool_end',
|
|
73
|
+
call,
|
|
74
|
+
ok: result.ok,
|
|
75
|
+
output: result.output,
|
|
76
|
+
ms: result.ms,
|
|
77
|
+
}),
|
|
78
|
+
approve: this.approval === 'auto' ? async () => true : this.approve,
|
|
79
|
+
onNotice: (notice) => this.onEvent({ type: 'notice', text: notice }),
|
|
80
|
+
onAssistantMessage: (message) => recorder.record(message),
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
const messages = outcome.messages.filter((message) => message.role !== 'system');
|
|
84
|
+
this.record.messages = messages;
|
|
85
|
+
this.record.meta.title = titleFromMessages(messages);
|
|
86
|
+
recorder.commit(messages);
|
|
87
|
+
this.onEvent({ type: 'done', usage: outcome.usage });
|
|
88
|
+
const last = [...messages]
|
|
89
|
+
.reverse()
|
|
90
|
+
.find((message) => message.role === 'assistant' && message.content.length > 0);
|
|
91
|
+
return {
|
|
92
|
+
text: last && last.role === 'assistant' ? last.content : '',
|
|
93
|
+
usage: outcome.usage,
|
|
94
|
+
messages: messages.length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
this.record.messages = recorder.snapshot();
|
|
99
|
+
this.record.meta.title = titleFromMessages(this.record.messages);
|
|
100
|
+
recorder.recover();
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const STALE_MS = 60_000;
|
|
4
|
+
const HEARTBEAT_MS = 15_000;
|
|
5
|
+
export function statusFile(dir, id) {
|
|
6
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
7
|
+
throw new Error('非法的会话 id');
|
|
8
|
+
}
|
|
9
|
+
return path.join(dir, id + '.status.json');
|
|
10
|
+
}
|
|
11
|
+
export function writeStatus(dir, status) {
|
|
12
|
+
// Invalid ids must fail loudly; filesystem problems are only advisory.
|
|
13
|
+
const file = statusFile(dir, status.sessionId);
|
|
14
|
+
try {
|
|
15
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
16
|
+
const payload = JSON.stringify(status);
|
|
17
|
+
const temporary = file + '.tmp';
|
|
18
|
+
fs.writeFileSync(temporary, payload, 'utf8');
|
|
19
|
+
try {
|
|
20
|
+
fs.renameSync(temporary, file);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
fs.writeFileSync(file, payload, 'utf8');
|
|
24
|
+
try {
|
|
25
|
+
fs.unlinkSync(temporary);
|
|
26
|
+
}
|
|
27
|
+
catch { /* best effort */ }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// Liveness is advisory; never break a turn over it.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export function readStatus(dir, id) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(fs.readFileSync(statusFile(dir, id), 'utf8'));
|
|
37
|
+
if (raw.sessionId !== id) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
if (raw.status !== 'idle' && raw.status !== 'running' && raw.status !== 'waiting_approval') {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (typeof raw.pid !== 'number' || typeof raw.updatedAt !== 'string') {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
sessionId: id,
|
|
48
|
+
status: raw.status,
|
|
49
|
+
pid: raw.pid,
|
|
50
|
+
updatedAt: raw.updatedAt,
|
|
51
|
+
...(typeof raw.title === 'string' ? { title: raw.title } : {}),
|
|
52
|
+
...(typeof raw.cwd === 'string' ? { cwd: raw.cwd } : {}),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function clearStatus(dir, id) {
|
|
60
|
+
try {
|
|
61
|
+
fs.unlinkSync(statusFile(dir, id));
|
|
62
|
+
}
|
|
63
|
+
catch { /* already gone */ }
|
|
64
|
+
}
|
|
65
|
+
export function listStatuses(dir) {
|
|
66
|
+
let names;
|
|
67
|
+
try {
|
|
68
|
+
names = fs.readdirSync(dir);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
const statuses = [];
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
if (!name.endsWith('.status.json')) {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
const status = readStatus(dir, name.slice(0, -'.status.json'.length));
|
|
79
|
+
if (status) {
|
|
80
|
+
statuses.push(status);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
statuses.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
84
|
+
return statuses;
|
|
85
|
+
}
|
|
86
|
+
export function isLive(status, now = Date.now(), staleMs = STALE_MS) {
|
|
87
|
+
if (!status) {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
const updated = Date.parse(status.updatedAt);
|
|
91
|
+
return Number.isFinite(updated) && now - updated <= staleMs;
|
|
92
|
+
}
|
|
93
|
+
export function statusLabel(status) {
|
|
94
|
+
switch (status) {
|
|
95
|
+
case 'running': return '正在跑';
|
|
96
|
+
case 'waiting_approval': return '等确认';
|
|
97
|
+
case 'idle': return '空闲';
|
|
98
|
+
default: return '离线';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Keeps one session's status file fresh while the terminal is open. */
|
|
102
|
+
export class SessionHeartbeat {
|
|
103
|
+
dir;
|
|
104
|
+
meta;
|
|
105
|
+
status = 'idle';
|
|
106
|
+
timer = null;
|
|
107
|
+
constructor(dir, meta) {
|
|
108
|
+
this.dir = dir;
|
|
109
|
+
this.meta = meta;
|
|
110
|
+
}
|
|
111
|
+
start() {
|
|
112
|
+
this.persist();
|
|
113
|
+
if (this.timer) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.timer = setInterval(() => this.persist(), HEARTBEAT_MS);
|
|
117
|
+
this.timer.unref?.();
|
|
118
|
+
}
|
|
119
|
+
setStatus(status) {
|
|
120
|
+
this.status = status;
|
|
121
|
+
this.persist();
|
|
122
|
+
}
|
|
123
|
+
/** Point the heartbeat at a new session after /new or /fork. */
|
|
124
|
+
retarget(meta) {
|
|
125
|
+
if (meta.id !== this.meta.id) {
|
|
126
|
+
clearStatus(this.dir, this.meta.id);
|
|
127
|
+
}
|
|
128
|
+
this.meta = meta;
|
|
129
|
+
this.persist();
|
|
130
|
+
}
|
|
131
|
+
stop() {
|
|
132
|
+
if (this.timer) {
|
|
133
|
+
clearInterval(this.timer);
|
|
134
|
+
this.timer = null;
|
|
135
|
+
}
|
|
136
|
+
clearStatus(this.dir, this.meta.id);
|
|
137
|
+
}
|
|
138
|
+
persist() {
|
|
139
|
+
writeStatus(this.dir, {
|
|
140
|
+
sessionId: this.meta.id,
|
|
141
|
+
status: this.status,
|
|
142
|
+
pid: process.pid,
|
|
143
|
+
updatedAt: new Date().toISOString(),
|
|
144
|
+
title: this.meta.title,
|
|
145
|
+
cwd: this.meta.cwd,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { repairTranscript } from "../agent/transcript.js";
|
|
2
|
+
/**
|
|
3
|
+
* Persists a turn after every assistant/tool message. A crash or an interrupt
|
|
4
|
+
* therefore leaves a readable session instead of losing the whole turn.
|
|
5
|
+
*/
|
|
6
|
+
export class TurnRecorder {
|
|
7
|
+
store;
|
|
8
|
+
meta;
|
|
9
|
+
base;
|
|
10
|
+
turn = [];
|
|
11
|
+
/** Last persistence error, if the disk became unavailable mid-turn. */
|
|
12
|
+
lastError = null;
|
|
13
|
+
constructor(store, meta, base) {
|
|
14
|
+
this.store = store;
|
|
15
|
+
this.meta = meta;
|
|
16
|
+
this.base = base.slice();
|
|
17
|
+
}
|
|
18
|
+
record(message) {
|
|
19
|
+
this.turn.push(message);
|
|
20
|
+
this.persist();
|
|
21
|
+
}
|
|
22
|
+
snapshot() {
|
|
23
|
+
return [...this.base, ...this.turn];
|
|
24
|
+
}
|
|
25
|
+
/** Repairs unanswered tool calls, persists the result and makes it the new base. */
|
|
26
|
+
recover() {
|
|
27
|
+
const repaired = repairTranscript(this.snapshot()).messages;
|
|
28
|
+
this.base = repaired;
|
|
29
|
+
this.turn = [];
|
|
30
|
+
this.persist();
|
|
31
|
+
return repaired;
|
|
32
|
+
}
|
|
33
|
+
/** Stores the final transcript (which may include a compaction summary). */
|
|
34
|
+
commit(finalMessages) {
|
|
35
|
+
this.base = finalMessages.slice();
|
|
36
|
+
this.turn = [];
|
|
37
|
+
this.persist();
|
|
38
|
+
}
|
|
39
|
+
persist() {
|
|
40
|
+
try {
|
|
41
|
+
this.store.save(this.meta, this.snapshot());
|
|
42
|
+
this.lastError = null;
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
// A full disk or a read-only home must not kill the conversation.
|
|
46
|
+
this.lastError = error;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { sessionsDir } from "../config/paths.js";
|
|
5
|
+
import { repairTranscript } from "../agent/transcript.js";
|
|
6
|
+
/** Sessions live in ~/.minko/sessions/<id>.jsonl: one meta line, then messages. */
|
|
7
|
+
export class SessionStore {
|
|
8
|
+
dir;
|
|
9
|
+
/** True once a write failed and the store moved to the temp fallback. */
|
|
10
|
+
fallback = false;
|
|
11
|
+
constructor(dir = sessionsDir()) {
|
|
12
|
+
this.dir = dir;
|
|
13
|
+
}
|
|
14
|
+
fileFor(id) {
|
|
15
|
+
if (!/^[A-Za-z0-9._-]+$/.test(id)) {
|
|
16
|
+
throw new Error('非法的会话 id');
|
|
17
|
+
}
|
|
18
|
+
return path.join(this.dir, id + '.jsonl');
|
|
19
|
+
}
|
|
20
|
+
create(cwd, model, id) {
|
|
21
|
+
const now = new Date().toISOString();
|
|
22
|
+
const meta = {
|
|
23
|
+
id: id ?? newSessionId(),
|
|
24
|
+
title: '新会话',
|
|
25
|
+
cwd,
|
|
26
|
+
model,
|
|
27
|
+
createdAt: now,
|
|
28
|
+
updatedAt: now,
|
|
29
|
+
};
|
|
30
|
+
this.write(meta, []);
|
|
31
|
+
return meta;
|
|
32
|
+
}
|
|
33
|
+
save(meta, messages) {
|
|
34
|
+
this.write({ ...meta, updatedAt: new Date().toISOString() }, messages);
|
|
35
|
+
}
|
|
36
|
+
/** Copies a session into a new branch; the source is left untouched. */
|
|
37
|
+
fork(id, name) {
|
|
38
|
+
const source = this.load(id);
|
|
39
|
+
if (!source) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
const now = new Date().toISOString();
|
|
43
|
+
const label = name?.trim() ?? '';
|
|
44
|
+
const meta = {
|
|
45
|
+
...source.meta,
|
|
46
|
+
id: newSessionId(),
|
|
47
|
+
title: label.length > 0 ? label : source.meta.title + '(分支)',
|
|
48
|
+
createdAt: now,
|
|
49
|
+
updatedAt: now,
|
|
50
|
+
forkedFrom: source.meta.id,
|
|
51
|
+
forkName: label.length > 0 ? label : undefined,
|
|
52
|
+
};
|
|
53
|
+
const messages = structuredClone(source.messages);
|
|
54
|
+
this.save(meta, messages);
|
|
55
|
+
return { meta, messages };
|
|
56
|
+
}
|
|
57
|
+
load(id) {
|
|
58
|
+
const file = this.fileFor(id);
|
|
59
|
+
if (!fs.existsSync(file)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n');
|
|
63
|
+
let meta = null;
|
|
64
|
+
const messages = [];
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (line.trim().length === 0) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
let record;
|
|
70
|
+
try {
|
|
71
|
+
record = JSON.parse(line);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (record.type === 'meta' && record.meta) {
|
|
77
|
+
meta = record.meta;
|
|
78
|
+
}
|
|
79
|
+
else if (record.type === 'message' && record.message) {
|
|
80
|
+
messages.push(record.message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
if (!meta) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
return { meta, messages: repairTranscript(messages).messages };
|
|
87
|
+
}
|
|
88
|
+
list() {
|
|
89
|
+
if (!fs.existsSync(this.dir)) {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
const metas = [];
|
|
93
|
+
for (const name of fs.readdirSync(this.dir)) {
|
|
94
|
+
if (!name.endsWith('.jsonl')) {
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
const record = this.load(name.slice(0, -6));
|
|
98
|
+
if (record) {
|
|
99
|
+
metas.push(record.meta);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
metas.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
103
|
+
return metas;
|
|
104
|
+
}
|
|
105
|
+
latest(cwd) {
|
|
106
|
+
const candidates = this.list();
|
|
107
|
+
if (cwd === undefined) {
|
|
108
|
+
return candidates.length > 0 ? candidates[0] : null;
|
|
109
|
+
}
|
|
110
|
+
const resolved = path.resolve(cwd);
|
|
111
|
+
return candidates.find((meta) => path.resolve(meta.cwd) === resolved) ?? null;
|
|
112
|
+
}
|
|
113
|
+
delete(id) {
|
|
114
|
+
const file = this.fileFor(id);
|
|
115
|
+
if (!fs.existsSync(file)) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
fs.unlinkSync(file);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
write(meta, messages) {
|
|
122
|
+
const lines = [JSON.stringify({ type: 'meta', meta })];
|
|
123
|
+
for (const message of messages) {
|
|
124
|
+
lines.push(JSON.stringify({ type: 'message', message }));
|
|
125
|
+
}
|
|
126
|
+
const payload = lines.join('\n') + '\n';
|
|
127
|
+
try {
|
|
128
|
+
this.writeTo(this.dir, meta.id, payload);
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
const fallback = fallbackSessionsDir();
|
|
132
|
+
if (this.dir === fallback) {
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
this.dir = fallback;
|
|
136
|
+
this.fallback = true;
|
|
137
|
+
this.writeTo(fallback, meta.id, payload);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
writeTo(dir, id, payload) {
|
|
141
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
142
|
+
fs.writeFileSync(path.join(dir, id + '.jsonl'), payload, 'utf8');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Some locked-down Windows profiles reject writes entirely; use temp then. */
|
|
146
|
+
export function fallbackSessionsDir() {
|
|
147
|
+
let user = 'user';
|
|
148
|
+
try {
|
|
149
|
+
user = os.userInfo().username || 'user';
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
user = 'user';
|
|
153
|
+
}
|
|
154
|
+
return path.join(os.tmpdir(), 'minko-' + user, 'sessions');
|
|
155
|
+
}
|
|
156
|
+
export function newSessionId(now = new Date()) {
|
|
157
|
+
const pad = (value, width = 2) => String(value).padStart(width, '0');
|
|
158
|
+
const stamp = String(now.getFullYear())
|
|
159
|
+
+ pad(now.getMonth() + 1) + pad(now.getDate()) + '-'
|
|
160
|
+
+ pad(now.getHours()) + pad(now.getMinutes()) + pad(now.getSeconds());
|
|
161
|
+
const random = Math.random().toString(36).slice(2, 6);
|
|
162
|
+
return stamp + '-' + random;
|
|
163
|
+
}
|
|
164
|
+
export function titleFromMessages(messages) {
|
|
165
|
+
const first = messages.find((message) => message.role === 'user');
|
|
166
|
+
if (!first || first.role !== 'user') {
|
|
167
|
+
return '新会话';
|
|
168
|
+
}
|
|
169
|
+
const line = first.content.replace(/\s+/g, ' ').trim();
|
|
170
|
+
return line.length > 40 ? line.slice(0, 40) + '…' : (line || '新会话');
|
|
171
|
+
}
|