@gajae-code/coding-agent 0.15.2 → 0.15.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -1
- package/dist/types/config/autorouting-tier-map.d.ts +0 -9
- package/dist/types/config/file-lock.d.ts +7 -1
- package/dist/types/config/keybindings.d.ts +13 -3
- package/dist/types/config/model-registry.d.ts +5 -0
- package/dist/types/config/model-resolver.d.ts +1 -0
- package/dist/types/config/settings-schema.d.ts +65 -1
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-state.d.ts +42 -2
- package/dist/types/extensibility/extensions/types.d.ts +2 -0
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +12 -0
- package/dist/types/gjc-runtime/workflow-placeholder.d.ts +6 -0
- package/dist/types/main.d.ts +1 -1
- package/dist/types/modes/action-registry.d.ts +1 -0
- package/dist/types/modes/components/hook-selector.d.ts +1 -0
- package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -1
- package/dist/types/modes/components/welcome.d.ts +2 -0
- package/dist/types/modes/controllers/input-controller.d.ts +16 -1
- package/dist/types/modes/controllers/runtime-mcp-command-controller.d.ts +42 -0
- package/dist/types/modes/interactive-mode.d.ts +7 -0
- package/dist/types/modes/shared/oauth-url-copy.d.ts +9 -0
- package/dist/types/modes/types.d.ts +3 -0
- package/dist/types/modes/ui-language.d.ts +2 -1
- package/dist/types/sdk/broker/lifecycle-ledger.d.ts +1 -0
- package/dist/types/sdk/broker/session-index.d.ts +18 -0
- package/dist/types/sdk/bus/index.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +2 -0
- package/dist/types/sdk/cli/session-cli.d.ts +1 -1
- package/dist/types/sdk/lifecycle/service.d.ts +32 -5
- package/dist/types/sdk/session.d.ts +5 -0
- package/dist/types/session/agent-session.d.ts +4 -0
- package/dist/types/session/fallback-chain-controller.d.ts +8 -0
- package/dist/types/slash-commands/helpers/aside.d.ts +54 -0
- package/dist/types/tools/ask.d.ts +9 -0
- package/dist/types/utils/clipboard.d.ts +9 -1
- package/package.json +7 -7
- package/scripts/generate-sdk-adapter-parity-manifest.ts +5 -2
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/commands/sdk.ts +2 -2
- package/src/config/autorouting-tier-map.ts +0 -9
- package/src/config/file-lock.ts +15 -1
- package/src/config/keybindings.ts +13 -3
- package/src/config/model-registry.ts +132 -21
- package/src/config/model-resolver.ts +2 -1
- package/src/config/models-config-schema.ts +2 -2
- package/src/config/settings-schema.ts +66 -1
- package/src/config/settings.ts +1 -1
- package/src/coordinator/contract.ts +1 -0
- package/src/coordinator-mcp/question-state.ts +235 -4
- package/src/coordinator-mcp/server.ts +402 -2
- package/src/dap/client.ts +19 -8
- package/src/deep-interview/render-middleware.ts +6 -2
- package/src/extensibility/extensions/types.ts +2 -0
- package/src/gjc-runtime/deep-interview-stage.ts +20 -1
- package/src/gjc-runtime/deep-interview-state.ts +3 -1
- package/src/gjc-runtime/ralplan-runtime.ts +6 -1
- package/src/gjc-runtime/session-state-lock.ts +11 -2
- package/src/gjc-runtime/ultragoal-runtime.ts +5 -0
- package/src/gjc-runtime/workflow-placeholder.ts +33 -0
- package/src/internal-urls/docs-index.generated.ts +11 -9
- package/src/lsp/client.ts +39 -4
- package/src/main.ts +42 -11
- package/src/modes/action-registry.ts +7 -0
- package/src/modes/components/hook-selector.ts +2 -1
- package/src/modes/components/iterm-pet-transport.ts +4 -2
- package/src/modes/components/runtime-mcp-add-wizard.ts +38 -1
- package/src/modes/components/settings-selector.ts +1 -0
- package/src/modes/components/tool-status-header.ts +149 -41
- package/src/modes/components/welcome.ts +8 -0
- package/src/modes/controllers/extension-ui-controller.ts +1 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +141 -7
- package/src/modes/controllers/plan-mode-controller.ts +12 -4
- package/src/modes/controllers/runtime-mcp-command-controller.ts +164 -20
- package/src/modes/controllers/selector-controller.ts +11 -2
- package/src/modes/interactive-mode.ts +88 -1
- package/src/modes/shared/oauth-url-copy.ts +33 -0
- package/src/modes/types.ts +3 -0
- package/src/modes/ui-language.ts +44 -3
- package/src/runtime-mcp/oauth-flow.ts +19 -8
- package/src/runtime-mcp/transports/stdio.ts +27 -13
- package/src/sdk/acp/adapter.ts +29 -2
- package/src/sdk/broker/broker.ts +26 -6
- package/src/sdk/broker/lifecycle-ledger.ts +28 -1
- package/src/sdk/broker/lifecycle.ts +664 -5
- package/src/sdk/broker/session-index.ts +246 -0
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/index.ts +135 -38
- package/src/sdk/bus/natives-augment.d.ts +20 -1
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +3 -0
- package/src/sdk/cli/session-cli.ts +34 -7
- package/src/sdk/lifecycle/service.ts +166 -11
- package/src/sdk/mcp/server.ts +2 -1
- package/src/sdk/protocol/operation-inventory.generated.json +40 -0
- package/src/sdk/protocol/operation-registry.ts +2 -1
- package/src/sdk/session.ts +91 -7
- package/src/session/agent-session.ts +473 -66
- package/src/session/fallback-chain-controller.ts +17 -0
- package/src/setup/frictionless-onboarding.ts +4 -4
- package/src/slash-commands/aside-command.test.ts +366 -0
- package/src/slash-commands/builtin-registry.ts +19 -0
- package/src/slash-commands/helpers/aside.ts +299 -0
- package/src/tools/ask-contract.ts +54 -3
- package/src/tools/ask.ts +54 -6
- package/src/utils/clipboard.ts +37 -28
|
@@ -155,6 +155,23 @@ export class FallbackChainController {
|
|
|
155
155
|
return this.advance() ? "advance" : "exhausted";
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
/**
|
|
159
|
+
* Record one bounded escaped-argument exhaustion for the active model and
|
|
160
|
+
* advance immediately. The discarded wire attempts are intentionally not
|
|
161
|
+
* provider charges; this single policy charge prevents the same model from
|
|
162
|
+
* being retried forever while still allowing every eligible fallback model
|
|
163
|
+
* its own literal-UTF-8 recovery budget.
|
|
164
|
+
*/
|
|
165
|
+
recordEscapedArgumentsFailure(reason: string, advance = true): boolean {
|
|
166
|
+
const selector = this.currentSelector();
|
|
167
|
+
if (!selector || this.exhaustedForTurn) return false;
|
|
168
|
+
this.#attemptStarted = false;
|
|
169
|
+
this.attemptsUsed = this.maxAttempts;
|
|
170
|
+
this.#totalAttemptsUsed += 1;
|
|
171
|
+
this.tried.push({ selector, triggerClass: "unknown", reason });
|
|
172
|
+
return advance ? this.advance() : this.activeIndex + 1 < this.chain.entries.length;
|
|
173
|
+
}
|
|
174
|
+
|
|
158
175
|
advance(): boolean {
|
|
159
176
|
if (this.exhaustedForTurn) return false;
|
|
160
177
|
this.activeIndex += 1;
|
|
@@ -196,10 +196,10 @@ function scriptScores(text: string): Map<string, number> {
|
|
|
196
196
|
const kana = counts.get("ja") ?? 0;
|
|
197
197
|
const han = counts.get("zh") ?? 0;
|
|
198
198
|
const scores = new Map<string, number>();
|
|
199
|
-
// Korean
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
//
|
|
199
|
+
// Korean Hangul is decisive only once it reaches the evidence minimum and is
|
|
200
|
+
// at least as strong as kana; an equal sub-threshold Hangul/kana pair leaves
|
|
201
|
+
// dominant Han evidence intact. Japanese kana claims mixed Han whenever it
|
|
202
|
+
// exceeds Hangul, including the lone-kana boundary below that minimum.
|
|
203
203
|
const koreanClaims = hangul >= kana && hangul >= LANGUAGE_EVIDENCE_MINIMUM;
|
|
204
204
|
const japaneseClaims = kana > hangul && kana >= LANGUAGE_EVIDENCE_MINIMUM;
|
|
205
205
|
if (hangul > 0 && hangul >= kana) addLanguageScore(scores, "ko", hangul);
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
ASIDE_INSTALL_COMMAND,
|
|
4
|
+
ASIDE_USAGE,
|
|
5
|
+
asideCliCandidates,
|
|
6
|
+
createAsideHandler,
|
|
7
|
+
formatAsideMcpRegistration,
|
|
8
|
+
parseAsideArgv,
|
|
9
|
+
posixQuote,
|
|
10
|
+
probeAsideCli,
|
|
11
|
+
resolveAsideCliPath,
|
|
12
|
+
windowsPowerShellQuote,
|
|
13
|
+
} from "./helpers/aside";
|
|
14
|
+
|
|
15
|
+
function runtimeHarness() {
|
|
16
|
+
const output: string[] = [];
|
|
17
|
+
return {
|
|
18
|
+
runtime: {
|
|
19
|
+
session: {},
|
|
20
|
+
settings: {},
|
|
21
|
+
cwd: "/tmp",
|
|
22
|
+
output: async (message: string) => {
|
|
23
|
+
output.push(message);
|
|
24
|
+
},
|
|
25
|
+
} as never,
|
|
26
|
+
output,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("Aside CLI probe", () => {
|
|
31
|
+
test("lists installer symlink then the macOS app bundle", () => {
|
|
32
|
+
expect(asideCliCandidates("/Users/demo")).toEqual([
|
|
33
|
+
"/Users/demo/.local/bin/aside",
|
|
34
|
+
"/Users/demo/.aside/cli/Aside CLI.app/Contents/MacOS/aside",
|
|
35
|
+
]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("prefers the installer symlink over PATH", () => {
|
|
39
|
+
const path = resolveAsideCliPath("/Users/demo", {
|
|
40
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
41
|
+
which: () => "/opt/homebrew/bin/aside",
|
|
42
|
+
});
|
|
43
|
+
expect(path).toBe("/Users/demo/.local/bin/aside");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("falls back to PATH when local candidates are missing", () => {
|
|
47
|
+
const probe = probeAsideCli("/Users/demo", {
|
|
48
|
+
isExecutable: filePath => filePath === "/opt/homebrew/bin/aside",
|
|
49
|
+
which: command => (command === "aside" ? "/opt/homebrew/bin/aside" : null),
|
|
50
|
+
});
|
|
51
|
+
expect(probe).toEqual({ ok: true, path: "/opt/homebrew/bin/aside" });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("reports searched locations when nothing is executable", () => {
|
|
55
|
+
const probe = probeAsideCli("/Users/demo", {
|
|
56
|
+
isExecutable: () => false,
|
|
57
|
+
which: () => null,
|
|
58
|
+
});
|
|
59
|
+
expect(probe.ok).toBe(false);
|
|
60
|
+
if (probe.ok) throw new Error("expected a miss");
|
|
61
|
+
expect(probe.searched).toContain("/Users/demo/.local/bin/aside");
|
|
62
|
+
expect(probe.searched).toContain("PATH (aside)");
|
|
63
|
+
expect(probe.manualInstallCommand).toBe(ASIDE_INSTALL_COMMAND);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("posixQuote", () => {
|
|
68
|
+
test("quotes paths that contain spaces", () => {
|
|
69
|
+
expect(posixQuote("/Users/demo/.aside/cli/Aside CLI.app/Contents/MacOS/aside")).toBe(
|
|
70
|
+
"'/Users/demo/.aside/cli/Aside CLI.app/Contents/MacOS/aside'",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("leaves simple paths unquoted", () => {
|
|
75
|
+
expect(posixQuote("/Users/demo/.local/bin/aside")).toBe("/Users/demo/.local/bin/aside");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("quotes native Windows paths for PowerShell", () => {
|
|
79
|
+
expect(windowsPowerShellQuote("C:\\Users\\demo\\Aside CLI\\aside.exe")).toBe(
|
|
80
|
+
"'C:\\Users\\demo\\Aside CLI\\aside.exe'",
|
|
81
|
+
);
|
|
82
|
+
expect(windowsPowerShellQuote("C:\\Users\\O'Brien\\aside.exe")).toBe("'C:\\Users\\O''Brien\\aside.exe'");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("selects PowerShell quoting for native Windows MCP commands", () => {
|
|
86
|
+
const cliPath = "C:\\Users\\demo\\Aside CLI\\aside.exe";
|
|
87
|
+
expect(formatAsideMcpRegistration(cliPath, "win32")).toContain(
|
|
88
|
+
`gjc mcp add aside ${windowsPowerShellQuote(cliPath)} mcp --project`,
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("parseAsideArgv", () => {
|
|
94
|
+
test("preserves empty quoted arguments and escaped quotes", () => {
|
|
95
|
+
expect(parseAsideArgv('exec --message "" it\\\'s')).toEqual({
|
|
96
|
+
ok: true,
|
|
97
|
+
args: ["exec", "--message", "", "it's"],
|
|
98
|
+
});
|
|
99
|
+
expect(parseAsideArgv('exec "say \\"hello\\""')).toEqual({
|
|
100
|
+
ok: true,
|
|
101
|
+
args: ["exec", 'say "hello"'],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("rejects unterminated quotes and escapes", () => {
|
|
106
|
+
expect(parseAsideArgv('exec "unterminated')).toEqual({ ok: false, error: "unterminated quote" });
|
|
107
|
+
expect(parseAsideArgv("exec trailing\\")).toEqual({ ok: false, error: "unfinished escape" });
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("/aside slash command", () => {
|
|
112
|
+
test("prints usage for help verbs without probing", async () => {
|
|
113
|
+
const handle = createAsideHandler({
|
|
114
|
+
homedir: () => "/missing",
|
|
115
|
+
isExecutable: () => false,
|
|
116
|
+
which: () => null,
|
|
117
|
+
exec: async () => {
|
|
118
|
+
throw new Error("exec should not run for /aside help");
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
const { runtime, output } = runtimeHarness();
|
|
122
|
+
expect(await handle({ name: "aside", args: "help", text: "/aside help" }, runtime)).toEqual({ consumed: true });
|
|
123
|
+
expect(output).toEqual([ASIDE_USAGE]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("guides installation when the CLI is missing", async () => {
|
|
127
|
+
const handle = createAsideHandler({
|
|
128
|
+
homedir: () => "/Users/demo",
|
|
129
|
+
isExecutable: () => false,
|
|
130
|
+
which: () => null,
|
|
131
|
+
exec: async () => {
|
|
132
|
+
throw new Error("exec should not run when CLI is missing");
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
const { runtime, output } = runtimeHarness();
|
|
136
|
+
expect(await handle({ name: "aside", args: "", text: "/aside" }, runtime)).toEqual({ consumed: true });
|
|
137
|
+
expect(output[0]).toContain("Aside CLI was not found.");
|
|
138
|
+
expect(output[0]).toContain(ASIDE_INSTALL_COMMAND);
|
|
139
|
+
expect(output[0]).toContain(ASIDE_USAGE);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("status prints the resolved path and version", async () => {
|
|
143
|
+
const execCalls: string[][] = [];
|
|
144
|
+
const handle = createAsideHandler({
|
|
145
|
+
homedir: () => "/Users/demo",
|
|
146
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
147
|
+
which: () => null,
|
|
148
|
+
exec: async (_command, args) => {
|
|
149
|
+
execCalls.push(args);
|
|
150
|
+
return { stdout: "1.26.810\n", stderr: "", code: 0, killed: false };
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
const { runtime, output } = runtimeHarness();
|
|
154
|
+
expect(await handle({ name: "aside", args: "", text: "/aside" }, runtime)).toEqual({ consumed: true });
|
|
155
|
+
expect(execCalls).toEqual([["--version"]]);
|
|
156
|
+
expect(output[0]).toContain("Aside CLI: /Users/demo/.local/bin/aside");
|
|
157
|
+
expect(output[0]).toContain("Version: 1.26.810");
|
|
158
|
+
expect(output[0]).toContain(ASIDE_USAGE);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test("sanitizes version output before rendering status", async () => {
|
|
162
|
+
const handle = createAsideHandler({
|
|
163
|
+
homedir: () => "/Users/demo",
|
|
164
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
165
|
+
which: () => null,
|
|
166
|
+
exec: async () => ({ stdout: "\x1b]0;pwned\x071.26.810\n", stderr: "", code: 0, killed: false }),
|
|
167
|
+
});
|
|
168
|
+
const { runtime, output } = runtimeHarness();
|
|
169
|
+
await handle({ name: "aside", args: "", text: "/aside" }, runtime);
|
|
170
|
+
expect(output[0]).toContain("Version: 1.26.810");
|
|
171
|
+
expect(output[0]).not.toContain("pwned");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test("freeform prompts run aside exec with a single prompt argument", async () => {
|
|
175
|
+
const execCalls: Array<{ command: string; args: string[] }> = [];
|
|
176
|
+
const handle = createAsideHandler({
|
|
177
|
+
homedir: () => "/Users/demo",
|
|
178
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
179
|
+
which: () => null,
|
|
180
|
+
exec: async (command, args) => {
|
|
181
|
+
execCalls.push({ command, args });
|
|
182
|
+
return { stdout: "ok", stderr: "", code: 0, killed: false };
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
const { runtime, output } = runtimeHarness();
|
|
186
|
+
expect(
|
|
187
|
+
await handle(
|
|
188
|
+
{ name: "aside", args: "Summarize the current page", text: "/aside Summarize the current page" },
|
|
189
|
+
runtime,
|
|
190
|
+
),
|
|
191
|
+
).toEqual({ consumed: true });
|
|
192
|
+
expect(execCalls).toEqual([
|
|
193
|
+
{ command: "/Users/demo/.local/bin/aside", args: ["exec", "Summarize the current page"] },
|
|
194
|
+
]);
|
|
195
|
+
expect(output).toEqual(["ok"]);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("exec passes quoted argv through without a shell", async () => {
|
|
199
|
+
const execCalls: string[][] = [];
|
|
200
|
+
const handle = createAsideHandler({
|
|
201
|
+
homedir: () => "/Users/demo",
|
|
202
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
203
|
+
which: () => null,
|
|
204
|
+
exec: async (_command, args) => {
|
|
205
|
+
execCalls.push(args);
|
|
206
|
+
return { stdout: "", stderr: "", code: 0, killed: false };
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
const { runtime, output } = runtimeHarness();
|
|
210
|
+
expect(
|
|
211
|
+
await handle(
|
|
212
|
+
{
|
|
213
|
+
name: "aside",
|
|
214
|
+
args: 'exec --session abc "Continue later"',
|
|
215
|
+
text: '/aside exec --session abc "Continue later"',
|
|
216
|
+
},
|
|
217
|
+
runtime,
|
|
218
|
+
),
|
|
219
|
+
).toEqual({ consumed: true });
|
|
220
|
+
expect(execCalls).toEqual([["exec", "--session", "abc", "Continue later"]]);
|
|
221
|
+
expect(output).toEqual(["Aside CLI exited 0 with no output."]);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("leading flags are forwarded to aside exec", async () => {
|
|
225
|
+
const execCalls: string[][] = [];
|
|
226
|
+
const handle = createAsideHandler({
|
|
227
|
+
homedir: () => "/Users/demo",
|
|
228
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
229
|
+
which: () => null,
|
|
230
|
+
exec: async (_command, args) => {
|
|
231
|
+
execCalls.push(args);
|
|
232
|
+
return { stdout: "continued", stderr: "", code: 0, killed: false };
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
const { runtime } = runtimeHarness();
|
|
236
|
+
await handle({ name: "aside", args: "--session abc Continue", text: "/aside --session abc Continue" }, runtime);
|
|
237
|
+
expect(execCalls).toEqual([["exec", "--session", "abc", "Continue"]]);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("mcp prints a quoted registration command and does not spawn stdio", async () => {
|
|
241
|
+
const handle = createAsideHandler({
|
|
242
|
+
homedir: () => "/Users/demo",
|
|
243
|
+
isExecutable: filePath => filePath.includes("Aside CLI.app"),
|
|
244
|
+
which: () => null,
|
|
245
|
+
exec: async () => {
|
|
246
|
+
throw new Error("mcp must not spawn aside mcp");
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
const { runtime, output } = runtimeHarness();
|
|
250
|
+
const cliPath = "/Users/demo/.aside/cli/Aside CLI.app/Contents/MacOS/aside";
|
|
251
|
+
expect(await handle({ name: "aside", args: "mcp", text: "/aside mcp" }, runtime)).toEqual({ consumed: true });
|
|
252
|
+
expect(output).toEqual([formatAsideMcpRegistration(cliPath)]);
|
|
253
|
+
expect(output[0]).toContain(`gjc mcp add aside ${posixQuote(cliPath)} mcp --project`);
|
|
254
|
+
expect(output[0]).not.toContain("aside mcp\n");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("rejects unexpected MCP arguments without probing or spawning", async () => {
|
|
258
|
+
const handle = createAsideHandler({
|
|
259
|
+
homedir: () => "/missing",
|
|
260
|
+
isExecutable: () => false,
|
|
261
|
+
which: () => null,
|
|
262
|
+
exec: async () => {
|
|
263
|
+
throw new Error("mcp must not spawn");
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
const { runtime, output } = runtimeHarness();
|
|
267
|
+
expect(await handle({ name: "aside", args: "mcp unexpected", text: "/aside mcp unexpected" }, runtime)).toEqual({
|
|
268
|
+
consumed: true,
|
|
269
|
+
});
|
|
270
|
+
expect(output).toEqual(["Usage: /aside mcp"]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("repl refuses to run inside GJC", async () => {
|
|
274
|
+
const handle = createAsideHandler({
|
|
275
|
+
homedir: () => "/Users/demo",
|
|
276
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
277
|
+
which: () => null,
|
|
278
|
+
exec: async () => {
|
|
279
|
+
throw new Error("repl must not spawn");
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const { runtime, output } = runtimeHarness();
|
|
283
|
+
expect(await handle({ name: "aside", args: "repl", text: "/aside repl" }, runtime)).toEqual({ consumed: true });
|
|
284
|
+
expect(output[0]).toContain("needs a real terminal TTY");
|
|
285
|
+
expect(output[0]).toContain("/Users/demo/.local/bin/aside repl");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("rejects unexpected REPL arguments without probing or spawning", async () => {
|
|
289
|
+
const handle = createAsideHandler({
|
|
290
|
+
homedir: () => "/missing",
|
|
291
|
+
isExecutable: () => false,
|
|
292
|
+
which: () => null,
|
|
293
|
+
exec: async () => {
|
|
294
|
+
throw new Error("repl must not spawn");
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
const { runtime, output } = runtimeHarness();
|
|
298
|
+
expect(await handle({ name: "aside", args: "repl unexpected", text: "/aside repl unexpected" }, runtime)).toEqual(
|
|
299
|
+
{
|
|
300
|
+
consumed: true,
|
|
301
|
+
},
|
|
302
|
+
);
|
|
303
|
+
expect(output).toEqual(["Usage: /aside repl"]);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("account forwards subcommands", async () => {
|
|
307
|
+
const execCalls: string[][] = [];
|
|
308
|
+
const handle = createAsideHandler({
|
|
309
|
+
homedir: () => "/Users/demo",
|
|
310
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
311
|
+
which: () => null,
|
|
312
|
+
exec: async (_command, args) => {
|
|
313
|
+
execCalls.push(args);
|
|
314
|
+
return { stdout: "u0", stderr: "", code: 0, killed: false };
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
const { runtime, output } = runtimeHarness();
|
|
318
|
+
expect(await handle({ name: "aside", args: "account list", text: "/aside account list" }, runtime)).toEqual({
|
|
319
|
+
consumed: true,
|
|
320
|
+
});
|
|
321
|
+
expect(execCalls).toEqual([["account", "list"]]);
|
|
322
|
+
expect(output).toEqual(["u0"]);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("nonzero CLI exit is consumed with a non-zero status", async () => {
|
|
326
|
+
const handle = createAsideHandler({
|
|
327
|
+
homedir: () => "/Users/demo",
|
|
328
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
329
|
+
which: () => null,
|
|
330
|
+
exec: async () => ({ stdout: "no daemon", stderr: "", code: 1, killed: false }),
|
|
331
|
+
});
|
|
332
|
+
const { runtime, output } = runtimeHarness();
|
|
333
|
+
expect(await handle({ name: "aside", args: "account status", text: "/aside account status" }, runtime)).toEqual({
|
|
334
|
+
consumed: true,
|
|
335
|
+
exitCode: 1,
|
|
336
|
+
});
|
|
337
|
+
expect(output).toEqual(["no daemon\n(exit 1)"]);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("timeout results are consumed with a non-zero status", async () => {
|
|
341
|
+
const handle = createAsideHandler({
|
|
342
|
+
homedir: () => "/Users/demo",
|
|
343
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
344
|
+
which: () => null,
|
|
345
|
+
exec: async () => ({ stdout: "partial", stderr: "", code: 0, killed: true }),
|
|
346
|
+
});
|
|
347
|
+
const { runtime, output } = runtimeHarness();
|
|
348
|
+
expect(await handle({ name: "aside", args: "exec Continue", text: "/aside exec Continue" }, runtime)).toEqual({
|
|
349
|
+
consumed: true,
|
|
350
|
+
exitCode: 1,
|
|
351
|
+
});
|
|
352
|
+
expect(output).toEqual(["partial\nAside CLI timed out or was cancelled."]);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
test("sanitizes CLI control sequences before rendering output", async () => {
|
|
356
|
+
const handle = createAsideHandler({
|
|
357
|
+
homedir: () => "/Users/demo",
|
|
358
|
+
isExecutable: filePath => filePath === "/Users/demo/.local/bin/aside",
|
|
359
|
+
which: () => null,
|
|
360
|
+
exec: async () => ({ stdout: "\x1b]0;pwned\x07safe", stderr: "", code: 0, killed: false }),
|
|
361
|
+
});
|
|
362
|
+
const { runtime, output } = runtimeHarness();
|
|
363
|
+
await handle({ name: "aside", args: "account status", text: "/aside account status" }, runtime);
|
|
364
|
+
expect(output).toEqual(["safe"]);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
} from "../setup/provider-onboarding";
|
|
46
46
|
import { parseThinkingLevel } from "../thinking";
|
|
47
47
|
import { getDisplayChangelogEntries } from "../utils/changelog";
|
|
48
|
+
import { handleAsideAcp } from "./helpers/aside";
|
|
48
49
|
import { buildAutoroutingStatusReport } from "./helpers/autorouting-status";
|
|
49
50
|
import { buildContextReportText } from "./helpers/context-report";
|
|
50
51
|
import { switchSessionCredentialCommand } from "./helpers/credential-switch";
|
|
@@ -609,6 +610,24 @@ const shutdownHandlerTui = (_command: ParsedSlashCommand, runtime: TuiSlashComma
|
|
|
609
610
|
const IMPORT_SESSION_RETAINED_DESCRIPTOR_AUTHORITY_AVAILABLE = process.platform === "linux";
|
|
610
611
|
|
|
611
612
|
const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
613
|
+
{
|
|
614
|
+
name: "aside",
|
|
615
|
+
priority: 32,
|
|
616
|
+
description: "Run the Aside CLI from the composer",
|
|
617
|
+
acpDescription: "Run the Aside CLI",
|
|
618
|
+
subcommands: [
|
|
619
|
+
{ name: "exec", description: "Run Aside with a prompt", usage: "[args] <prompt>" },
|
|
620
|
+
{ name: "repl", description: "Aside REPL is terminal-only; prints the outside command" },
|
|
621
|
+
{ name: "mcp", description: "Print MCP registration using the resolved CLI path" },
|
|
622
|
+
{ name: "account", description: "Inspect or select Aside CLI accounts", usage: "[list|status|use]" },
|
|
623
|
+
{ name: "help", description: "Show /aside usage" },
|
|
624
|
+
],
|
|
625
|
+
inlineHint: "[exec|repl|mcp|account|help|<prompt>]",
|
|
626
|
+
acpInputHint: "[exec|repl|mcp|account|help|<prompt>]",
|
|
627
|
+
allowArgs: true,
|
|
628
|
+
localHeadless: true,
|
|
629
|
+
handle: handleAsideAcp,
|
|
630
|
+
},
|
|
612
631
|
{
|
|
613
632
|
name: "import-session",
|
|
614
633
|
priority: 29,
|