@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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/dist/adapters/accounts.js +34 -0
  4. package/dist/adapters/auth/anthropic.js +36 -0
  5. package/dist/adapters/auth/chatgpt.js +100 -0
  6. package/dist/adapters/auth/oauth.js +68 -0
  7. package/dist/adapters/auth/openrouter.js +27 -0
  8. package/dist/adapters/auth/store.js +25 -0
  9. package/dist/adapters/git/worktree.js +113 -0
  10. package/dist/adapters/mcp/config.js +59 -0
  11. package/dist/adapters/mcp/manager.js +165 -0
  12. package/dist/adapters/mcp/results.js +21 -0
  13. package/dist/adapters/mcp/transport.js +19 -0
  14. package/dist/adapters/project.js +15 -0
  15. package/dist/adapters/providers/anthropic.js +103 -0
  16. package/dist/adapters/providers/catalog.js +80 -0
  17. package/dist/adapters/providers/chatgpt.js +117 -0
  18. package/dist/adapters/providers/credentials.js +55 -0
  19. package/dist/adapters/providers/gemini.js +123 -0
  20. package/dist/adapters/providers/openai.js +128 -0
  21. package/dist/adapters/providers/registry.js +68 -0
  22. package/dist/adapters/providers/shared.js +12 -0
  23. package/dist/adapters/providers/usage.js +95 -0
  24. package/dist/adapters/settings.js +15 -0
  25. package/dist/adapters/skills.js +163 -0
  26. package/dist/adapters/storage.js +30 -0
  27. package/dist/adapters/tools/files.js +113 -0
  28. package/dist/adapters/tools/image.js +63 -0
  29. package/dist/adapters/tools/index.js +26 -0
  30. package/dist/adapters/tools/output.js +56 -0
  31. package/dist/adapters/tools/paths.js +5 -0
  32. package/dist/adapters/tools/questions.js +52 -0
  33. package/dist/adapters/tools/shell.js +73 -0
  34. package/dist/adapters/tools/validation.js +34 -0
  35. package/dist/args.js +30 -0
  36. package/dist/cli.js +111 -0
  37. package/dist/composition.js +18 -0
  38. package/dist/core/agent.js +128 -0
  39. package/dist/core/conversation.js +37 -0
  40. package/dist/core/prompts.js +12 -0
  41. package/dist/core/provider.js +8 -0
  42. package/dist/core/settings.js +15 -0
  43. package/dist/core/suggestion.js +29 -0
  44. package/dist/core/tools.js +10 -0
  45. package/dist/lib/async.js +6 -0
  46. package/dist/ui/App.js +218 -0
  47. package/dist/ui/ApprovalDialog.js +39 -0
  48. package/dist/ui/ConfigMenu.js +106 -0
  49. package/dist/ui/Dialog.js +10 -0
  50. package/dist/ui/DiffLines.js +12 -0
  51. package/dist/ui/EffortPicker.js +8 -0
  52. package/dist/ui/Help.js +21 -0
  53. package/dist/ui/KeyList.js +10 -0
  54. package/dist/ui/LoginDialog.js +154 -0
  55. package/dist/ui/McpMenu.js +138 -0
  56. package/dist/ui/ModelPicker.js +117 -0
  57. package/dist/ui/PromptInput.js +161 -0
  58. package/dist/ui/Questionnaire.js +35 -0
  59. package/dist/ui/Select.js +28 -0
  60. package/dist/ui/Spinner.js +18 -0
  61. package/dist/ui/StatusLine.js +19 -0
  62. package/dist/ui/TextField.js +22 -0
  63. package/dist/ui/Transcript.js +40 -0
  64. package/dist/ui/WorktreeMenu.js +83 -0
  65. package/dist/ui/autocomplete.js +6 -0
  66. package/dist/ui/commands.js +117 -0
  67. package/dist/ui/diff.js +130 -0
  68. package/dist/ui/format.js +138 -0
  69. package/dist/ui/history.js +9 -0
  70. package/dist/ui/hooks/useAgentSession.js +34 -0
  71. package/dist/ui/hooks/usePromptSuggestion.js +23 -0
  72. package/dist/ui/hooks/useStreamedText.js +38 -0
  73. package/dist/ui/hooks/useWorktree.js +30 -0
  74. package/dist/ui/loginFlow.js +37 -0
  75. package/dist/ui/mcpWizard.js +18 -0
  76. package/dist/ui/plain.js +60 -0
  77. package/dist/ui/session.js +168 -0
  78. 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
+ }