@megacode/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/dist/adapters/accounts.js +34 -0
- package/dist/adapters/auth/anthropic.js +36 -0
- package/dist/adapters/auth/chatgpt.js +100 -0
- package/dist/adapters/auth/oauth.js +68 -0
- package/dist/adapters/auth/openrouter.js +27 -0
- package/dist/adapters/auth/store.js +25 -0
- package/dist/adapters/git/worktree.js +113 -0
- package/dist/adapters/mcp/config.js +59 -0
- package/dist/adapters/mcp/manager.js +165 -0
- package/dist/adapters/mcp/results.js +21 -0
- package/dist/adapters/mcp/transport.js +19 -0
- package/dist/adapters/project.js +15 -0
- package/dist/adapters/providers/anthropic.js +103 -0
- package/dist/adapters/providers/catalog.js +80 -0
- package/dist/adapters/providers/chatgpt.js +117 -0
- package/dist/adapters/providers/credentials.js +55 -0
- package/dist/adapters/providers/gemini.js +123 -0
- package/dist/adapters/providers/openai.js +128 -0
- package/dist/adapters/providers/registry.js +68 -0
- package/dist/adapters/providers/shared.js +12 -0
- package/dist/adapters/providers/usage.js +95 -0
- package/dist/adapters/settings.js +15 -0
- package/dist/adapters/skills.js +163 -0
- package/dist/adapters/storage.js +30 -0
- package/dist/adapters/tools/files.js +113 -0
- package/dist/adapters/tools/image.js +63 -0
- package/dist/adapters/tools/index.js +26 -0
- package/dist/adapters/tools/output.js +56 -0
- package/dist/adapters/tools/paths.js +5 -0
- package/dist/adapters/tools/questions.js +52 -0
- package/dist/adapters/tools/shell.js +73 -0
- package/dist/adapters/tools/validation.js +34 -0
- package/dist/args.js +30 -0
- package/dist/cli.js +111 -0
- package/dist/composition.js +18 -0
- package/dist/core/agent.js +128 -0
- package/dist/core/conversation.js +37 -0
- package/dist/core/prompts.js +12 -0
- package/dist/core/provider.js +8 -0
- package/dist/core/settings.js +15 -0
- package/dist/core/suggestion.js +29 -0
- package/dist/core/tools.js +10 -0
- package/dist/lib/async.js +6 -0
- package/dist/ui/App.js +218 -0
- package/dist/ui/ApprovalDialog.js +39 -0
- package/dist/ui/ConfigMenu.js +106 -0
- package/dist/ui/Dialog.js +10 -0
- package/dist/ui/DiffLines.js +12 -0
- package/dist/ui/EffortPicker.js +8 -0
- package/dist/ui/Help.js +21 -0
- package/dist/ui/KeyList.js +10 -0
- package/dist/ui/LoginDialog.js +154 -0
- package/dist/ui/McpMenu.js +138 -0
- package/dist/ui/ModelPicker.js +117 -0
- package/dist/ui/PromptInput.js +161 -0
- package/dist/ui/Questionnaire.js +35 -0
- package/dist/ui/Select.js +28 -0
- package/dist/ui/Spinner.js +18 -0
- package/dist/ui/StatusLine.js +19 -0
- package/dist/ui/TextField.js +22 -0
- package/dist/ui/Transcript.js +40 -0
- package/dist/ui/WorktreeMenu.js +83 -0
- package/dist/ui/autocomplete.js +6 -0
- package/dist/ui/commands.js +117 -0
- package/dist/ui/diff.js +130 -0
- package/dist/ui/format.js +138 -0
- package/dist/ui/history.js +9 -0
- package/dist/ui/hooks/useAgentSession.js +34 -0
- package/dist/ui/hooks/usePromptSuggestion.js +23 -0
- package/dist/ui/hooks/useStreamedText.js +38 -0
- package/dist/ui/hooks/useWorktree.js +30 -0
- package/dist/ui/loginFlow.js +37 -0
- package/dist/ui/mcpWizard.js +18 -0
- package/dist/ui/plain.js +60 -0
- package/dist/ui/session.js +168 -0
- package/package.json +57 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { providerInfo } from "../adapters/providers/catalog.js";
|
|
2
|
+
import { providerOf } from "../adapters/providers/credentials.js";
|
|
3
|
+
import { autoApproved, EDIT_TOOLS } from "../core/settings.js";
|
|
4
|
+
import { buildDiff } from "./diff.js";
|
|
5
|
+
import { displayOutput } from "./format.js";
|
|
6
|
+
const VERBS = ["Thinking", "Pondering", "Working", "Crafting", "Computing", "Tinkering"];
|
|
7
|
+
/**
|
|
8
|
+
* One conversation with the agent, independent of React: running turns, queueing messages typed
|
|
9
|
+
* meanwhile, interrupting, and answering the agent's approvals and questionnaires.
|
|
10
|
+
* State is an immutable snapshot; `subscribe` reports changes.
|
|
11
|
+
*/
|
|
12
|
+
export class AgentSession {
|
|
13
|
+
#state = {
|
|
14
|
+
running: false,
|
|
15
|
+
activeTool: null,
|
|
16
|
+
approval: null,
|
|
17
|
+
questionnaire: null,
|
|
18
|
+
queued: [],
|
|
19
|
+
verb: VERBS[0],
|
|
20
|
+
completedTurns: 0,
|
|
21
|
+
};
|
|
22
|
+
#listeners = new Set();
|
|
23
|
+
#controller = null;
|
|
24
|
+
#denied = false;
|
|
25
|
+
#sendNow = false; // interrupted to send the queue: run it instead of handing it back
|
|
26
|
+
#alwaysAllow = new Set();
|
|
27
|
+
#agent;
|
|
28
|
+
#host;
|
|
29
|
+
constructor(agent, host) {
|
|
30
|
+
this.#agent = agent;
|
|
31
|
+
this.#host = host;
|
|
32
|
+
}
|
|
33
|
+
get state() {
|
|
34
|
+
return this.#state;
|
|
35
|
+
}
|
|
36
|
+
subscribe = (listener) => {
|
|
37
|
+
this.#listeners.add(listener);
|
|
38
|
+
return () => void this.#listeners.delete(listener);
|
|
39
|
+
};
|
|
40
|
+
#update(patch) {
|
|
41
|
+
this.#state = { ...this.#state, ...patch };
|
|
42
|
+
for (const listener of this.#listeners)
|
|
43
|
+
listener();
|
|
44
|
+
}
|
|
45
|
+
/** Runs the message now, or queues it for after the current turn. Resolves when the session is idle again. */
|
|
46
|
+
submit = (text) => {
|
|
47
|
+
if (!this.#controller)
|
|
48
|
+
return this.#send(text);
|
|
49
|
+
this.#update({ queued: [...this.#state.queued, text] });
|
|
50
|
+
return Promise.resolve();
|
|
51
|
+
};
|
|
52
|
+
/** Stops the running turn and denies a pending approval. Queued messages go back to the input. */
|
|
53
|
+
interrupt = () => {
|
|
54
|
+
const { approval } = this.#state;
|
|
55
|
+
if (approval) {
|
|
56
|
+
approval.resolve(false);
|
|
57
|
+
this.#update({ approval: null });
|
|
58
|
+
}
|
|
59
|
+
this.#controller?.abort();
|
|
60
|
+
};
|
|
61
|
+
/** Adds `extra` to the queue, then stops the running turn so the queue is sent right away. */
|
|
62
|
+
flushQueue = (extra) => {
|
|
63
|
+
if (extra)
|
|
64
|
+
this.#update({ queued: [...this.#state.queued, extra] });
|
|
65
|
+
if (!this.#state.queued.length)
|
|
66
|
+
return;
|
|
67
|
+
this.#sendNow = true;
|
|
68
|
+
this.interrupt();
|
|
69
|
+
};
|
|
70
|
+
answerApproval = (choice) => {
|
|
71
|
+
const { approval } = this.#state;
|
|
72
|
+
if (!approval)
|
|
73
|
+
return;
|
|
74
|
+
if (choice === "always") {
|
|
75
|
+
if (EDIT_TOOLS.has(approval.tool))
|
|
76
|
+
this.#host.onAllowEdits();
|
|
77
|
+
else
|
|
78
|
+
this.#alwaysAllow.add(approval.tool);
|
|
79
|
+
}
|
|
80
|
+
approval.resolve(choice !== "no");
|
|
81
|
+
this.#update({ approval: null });
|
|
82
|
+
if (choice === "no") {
|
|
83
|
+
this.#denied = true;
|
|
84
|
+
this.#controller?.abort();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/** Aborts any running turn without reporting it, e.g. when the app exits. */
|
|
88
|
+
abort = () => this.#controller?.abort();
|
|
89
|
+
async #send(text) {
|
|
90
|
+
const host = this.#host;
|
|
91
|
+
if (!host.canSend(text))
|
|
92
|
+
return;
|
|
93
|
+
host.push({ kind: "user", text });
|
|
94
|
+
const ctrl = (this.#controller = new AbortController());
|
|
95
|
+
this.#denied = false;
|
|
96
|
+
this.#sendNow = false;
|
|
97
|
+
this.#update({ running: true, verb: VERBS[Math.floor(Math.random() * VERBS.length)] });
|
|
98
|
+
let interrupted = false;
|
|
99
|
+
try {
|
|
100
|
+
await this.#agent.send(text, ctrl.signal, this.#events());
|
|
101
|
+
if (!ctrl.signal.aborted)
|
|
102
|
+
this.#update({ completedTurns: this.#state.completedTurns + 1 });
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
host.onStepEnd(); // commit any half-streamed text
|
|
106
|
+
interrupted = ctrl.signal.aborted;
|
|
107
|
+
this.#reportFailure(e, interrupted);
|
|
108
|
+
}
|
|
109
|
+
finally {
|
|
110
|
+
this.#controller = null;
|
|
111
|
+
this.#update({ running: false, activeTool: null });
|
|
112
|
+
}
|
|
113
|
+
// Send queued messages next; after an interrupt (other than flushQueue), hand them back to the input instead.
|
|
114
|
+
const next = this.#state.queued.join("\n");
|
|
115
|
+
this.#update({ queued: [] });
|
|
116
|
+
if (next && interrupted && !this.#sendNow)
|
|
117
|
+
host.restoreInput(next);
|
|
118
|
+
else if (next)
|
|
119
|
+
await this.#send(next);
|
|
120
|
+
}
|
|
121
|
+
#reportFailure(error, interrupted) {
|
|
122
|
+
const { notice } = this.#host;
|
|
123
|
+
if (this.#denied)
|
|
124
|
+
notice("Denied. Tell megacode what to do instead.", "warn");
|
|
125
|
+
else if (interrupted && this.#sendNow)
|
|
126
|
+
notice("Interrupted to send queued messages.");
|
|
127
|
+
else if (interrupted)
|
|
128
|
+
notice("Interrupted. What should megacode do instead?", "warn");
|
|
129
|
+
else if ([401, 403].includes(error.status))
|
|
130
|
+
notice(`${providerInfo(providerOf(this.#agent.model)).label} rejected the credentials. Run /login to update them.`, "error");
|
|
131
|
+
else
|
|
132
|
+
notice(error.message, "error");
|
|
133
|
+
}
|
|
134
|
+
#approve = (req) => {
|
|
135
|
+
if (autoApproved(this.#host.mode(), req.tool) || this.#alwaysAllow.has(req.tool))
|
|
136
|
+
return Promise.resolve(true);
|
|
137
|
+
return new Promise((resolve) => this.#update({ approval: { ...req, resolve } }));
|
|
138
|
+
};
|
|
139
|
+
#askQuestions = (questions, signal) => new Promise((resolve) => {
|
|
140
|
+
if (signal?.aborted)
|
|
141
|
+
return resolve(null);
|
|
142
|
+
const finish = (answers) => {
|
|
143
|
+
signal?.removeEventListener("abort", abort);
|
|
144
|
+
this.#update({ questionnaire: null });
|
|
145
|
+
resolve(answers);
|
|
146
|
+
};
|
|
147
|
+
const abort = () => finish(null);
|
|
148
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
149
|
+
this.#update({ questionnaire: { questions, resolve: finish } });
|
|
150
|
+
});
|
|
151
|
+
#events() {
|
|
152
|
+
const host = this.#host;
|
|
153
|
+
return {
|
|
154
|
+
approve: this.#approve,
|
|
155
|
+
askQuestions: this.#askQuestions,
|
|
156
|
+
onText: (delta) => host.onText(delta),
|
|
157
|
+
onStepEnd: () => host.onStepEnd(),
|
|
158
|
+
onToolStart: (call) => this.#update({ activeTool: call }),
|
|
159
|
+
onToolEnd: (call, r) => {
|
|
160
|
+
this.#update({ activeTool: null });
|
|
161
|
+
// The transcript shows the whole edit; a new file shows its first lines, as in Claude Code.
|
|
162
|
+
const diff = r.change && !r.isError ? buildDiff(r.change, r.change.created ? 10 : 60) : undefined;
|
|
163
|
+
host.push({ kind: "tool", call, output: displayOutput(call, r, diff), isError: r.isError, diff: diff ?? undefined });
|
|
164
|
+
},
|
|
165
|
+
onNotice: (text, level) => host.notice(text, level),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@megacode/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Minimal multi-provider coding agent for the terminal (Anthropic, OpenAI, Gemini, OpenAI-compatible)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"agent",
|
|
8
|
+
"coding-agent",
|
|
9
|
+
"cli",
|
|
10
|
+
"llm",
|
|
11
|
+
"anthropic",
|
|
12
|
+
"claude",
|
|
13
|
+
"openai",
|
|
14
|
+
"gemini",
|
|
15
|
+
"ollama"
|
|
16
|
+
],
|
|
17
|
+
"author": "Michal Jach",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"type": "module",
|
|
20
|
+
"bin": {
|
|
21
|
+
"megacode": "dist/cli.js"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.14"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"start": "tsx src/cli.tsx",
|
|
34
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
35
|
+
"typecheck": "tsc",
|
|
36
|
+
"test": "node --import tsx --test test/*.test.ts test/*.test.mjs",
|
|
37
|
+
"bench": "tsx bench/run.ts",
|
|
38
|
+
"prepublishOnly": "npm test && npm run typecheck && npm run build"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@anthropic-ai/sdk": "^0.128.0",
|
|
42
|
+
"@google/genai": "^2.24.0",
|
|
43
|
+
"@modelcontextprotocol/sdk": "^1.32.0",
|
|
44
|
+
"chalk": "^5.6.2",
|
|
45
|
+
"diff": "^9.0.0",
|
|
46
|
+
"ink": "^7.1.1",
|
|
47
|
+
"openai": "^7.23.0",
|
|
48
|
+
"react": "^19.3.0",
|
|
49
|
+
"yaml": "^2.9.1"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@types/node": "^26.6.3",
|
|
53
|
+
"@types/react": "^19.3.0",
|
|
54
|
+
"tsx": "^4.23.15",
|
|
55
|
+
"typescript": "^7.0.2"
|
|
56
|
+
}
|
|
57
|
+
}
|