@f5-sales-demo/xcsh 19.95.3 → 19.97.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/package.json +8 -8
- package/src/browser/capabilities.generated.ts +7 -3
- package/src/browser/capabilities.json +6 -2
- package/src/browser/chat-conformance.json +140 -1
- package/src/browser/chat-handler.ts +24 -1
- package/src/browser/chat-protocol.ts +28 -0
- package/src/browser/host-profiles.ts +5 -2
- package/src/cli/args.ts +233 -131
- package/src/cli/flag-spec.ts +219 -0
- package/src/cli/office-cli.ts +26 -3
- package/src/commands/launch.ts +3 -105
- package/src/config/settings-schema.ts +9 -2
- package/src/extensibility/extensions/bundled/sandbox-guard.ts +0 -0
- package/src/extensibility/skills.ts +27 -7
- package/src/extensibility/slash-commands.ts +20 -0
- package/src/internal-urls/build-info-runtime.ts +21 -2
- package/src/internal-urls/build-info.generated.ts +8 -8
- package/src/internal-urls/plugin-resolve.test.ts +61 -0
- package/src/internal-urls/plugin-resolve.ts +63 -9
- package/src/internal-urls/xcsh-protocol.active-model.test.ts +53 -0
- package/src/internal-urls/xcsh-protocol.ts +10 -1
- package/src/main.ts +50 -23
- package/src/modes/print-mode.ts +21 -6
- package/src/modes/rpc/rpc-client.ts +10 -0
- package/src/modes/rpc/rpc-mode.ts +8 -0
- package/src/modes/rpc/rpc-types.ts +9 -0
- package/src/prompts/internal-urls/active-model.md +32 -0
- package/src/sandbox/command-operands.ts +280 -0
- package/src/sandbox/enforce.ts +48 -4
- package/src/sandbox/session-policy.ts +0 -0
- package/src/sdk.ts +16 -0
- package/src/session/active-model.ts +86 -0
- package/src/session/agent-session.ts +29 -2
- package/src/tools/bash-skill-urls.ts +114 -61
- package/src/tools/bash.ts +24 -21
- package/src/tools/shell-lex.ts +652 -0
package/src/cli/args.ts
CHANGED
|
@@ -6,6 +6,14 @@ import { APP_NAME, CONFIG_DIR_NAME, logger } from "@f5-sales-demo/pi-utils";
|
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { parseEffort } from "../thinking";
|
|
8
8
|
import { BUILTIN_TOOLS } from "../tools";
|
|
9
|
+
import {
|
|
10
|
+
flagNameForChar,
|
|
11
|
+
flagSpec,
|
|
12
|
+
type LaunchFlagName,
|
|
13
|
+
normalizeFlagTokens,
|
|
14
|
+
takesValue,
|
|
15
|
+
type UnrecognizedFlag,
|
|
16
|
+
} from "./flag-spec";
|
|
9
17
|
|
|
10
18
|
export type Mode = "text" | "json" | "rpc" | "acp";
|
|
11
19
|
|
|
@@ -55,6 +63,169 @@ export interface Args {
|
|
|
55
63
|
fileArgs: string[];
|
|
56
64
|
/** Unknown flags (potentially extension flags) - map of flag name to value */
|
|
57
65
|
unknownFlags: Map<string, boolean | string>;
|
|
66
|
+
/**
|
|
67
|
+
* Flags matched by neither the spec nor a known extension flag.
|
|
68
|
+
*
|
|
69
|
+
* Collected rather than reported here: the extension flag registry does not exist yet during the
|
|
70
|
+
* first parse, so only `main.ts` can tell a genuine typo from a flag an extension will claim.
|
|
71
|
+
*/
|
|
72
|
+
unrecognizedFlags: UnrecognizedFlag[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Apply one parsed flag to the result, keeping each flag's own value coercion.
|
|
77
|
+
*
|
|
78
|
+
* Split from the scanning loop so arity, aliasing and the `=` form are handled once, in one place,
|
|
79
|
+
* rather than repeated per flag as an `arg === "--x" && i + 1 < args.length` chain — which is how
|
|
80
|
+
* `--x=value` came to match nothing at all and vanish silently (#2469).
|
|
81
|
+
*/
|
|
82
|
+
const APPLY: Record<LaunchFlagName, (result: Args, value: string | true) => void> = {
|
|
83
|
+
model: (r, v) => {
|
|
84
|
+
r.model = v as string;
|
|
85
|
+
},
|
|
86
|
+
smol: (r, v) => {
|
|
87
|
+
r.smol = v as string;
|
|
88
|
+
},
|
|
89
|
+
slow: (r, v) => {
|
|
90
|
+
r.slow = v as string;
|
|
91
|
+
},
|
|
92
|
+
plan: (r, v) => {
|
|
93
|
+
r.plan = v as string;
|
|
94
|
+
},
|
|
95
|
+
provider: (r, v) => {
|
|
96
|
+
r.provider = v as string;
|
|
97
|
+
},
|
|
98
|
+
"api-key": (r, v) => {
|
|
99
|
+
r.apiKey = v as string;
|
|
100
|
+
},
|
|
101
|
+
"system-prompt": (r, v) => {
|
|
102
|
+
r.systemPrompt = v as string;
|
|
103
|
+
},
|
|
104
|
+
"append-system-prompt": (r, v) => {
|
|
105
|
+
r.appendSystemPrompt = v as string;
|
|
106
|
+
},
|
|
107
|
+
"allow-home": r => {
|
|
108
|
+
r.allowHome = true;
|
|
109
|
+
},
|
|
110
|
+
"no-sandbox": r => {
|
|
111
|
+
r.noSandbox = true;
|
|
112
|
+
},
|
|
113
|
+
"allow-path": (r, v) => {
|
|
114
|
+
r.allowPath = [...(r.allowPath ?? []), v as string];
|
|
115
|
+
},
|
|
116
|
+
mode: (r, v) => {
|
|
117
|
+
// Already validated against the spec's `options`.
|
|
118
|
+
r.mode = v as Mode;
|
|
119
|
+
},
|
|
120
|
+
print: r => {
|
|
121
|
+
r.print = true;
|
|
122
|
+
},
|
|
123
|
+
continue: r => {
|
|
124
|
+
r.continue = true;
|
|
125
|
+
},
|
|
126
|
+
resume: (r, v) => {
|
|
127
|
+
r.resume = v;
|
|
128
|
+
},
|
|
129
|
+
session: (r, v) => {
|
|
130
|
+
r.resume = v;
|
|
131
|
+
},
|
|
132
|
+
fork: (r, v) => {
|
|
133
|
+
r.fork = v as string;
|
|
134
|
+
},
|
|
135
|
+
"session-dir": (r, v) => {
|
|
136
|
+
r.sessionDir = v as string;
|
|
137
|
+
},
|
|
138
|
+
"no-session": r => {
|
|
139
|
+
r.noSession = true;
|
|
140
|
+
},
|
|
141
|
+
"provider-session-id": (r, v) => {
|
|
142
|
+
r.providerSessionId = v as string;
|
|
143
|
+
},
|
|
144
|
+
models: (r, v) => {
|
|
145
|
+
r.models = (v as string).split(",").map(s => s.trim());
|
|
146
|
+
},
|
|
147
|
+
"no-tools": r => {
|
|
148
|
+
r.noTools = true;
|
|
149
|
+
},
|
|
150
|
+
"no-mcp": r => {
|
|
151
|
+
r.noMcp = true;
|
|
152
|
+
},
|
|
153
|
+
"no-lsp": r => {
|
|
154
|
+
r.noLsp = true;
|
|
155
|
+
},
|
|
156
|
+
"no-pty": r => {
|
|
157
|
+
r.noPty = true;
|
|
158
|
+
},
|
|
159
|
+
tools: (r, v) => {
|
|
160
|
+
const toolNames = (v as string)
|
|
161
|
+
.split(",")
|
|
162
|
+
.map(s => s.trim().toLowerCase())
|
|
163
|
+
.filter(Boolean);
|
|
164
|
+
const validTools: string[] = [];
|
|
165
|
+
for (const name of toolNames) {
|
|
166
|
+
if (name in BUILTIN_TOOLS) {
|
|
167
|
+
validTools.push(name);
|
|
168
|
+
} else {
|
|
169
|
+
logger.warn("Unknown tool passed to --tools", {
|
|
170
|
+
tool: name,
|
|
171
|
+
validTools: Object.keys(BUILTIN_TOOLS),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
r.tools = validTools;
|
|
176
|
+
},
|
|
177
|
+
thinking: (r, v) => {
|
|
178
|
+
const thinking = parseEffort(v as string);
|
|
179
|
+
if (thinking !== undefined) {
|
|
180
|
+
r.thinking = thinking;
|
|
181
|
+
} else {
|
|
182
|
+
logger.warn("Invalid thinking level passed to --thinking", {
|
|
183
|
+
level: v,
|
|
184
|
+
validThinkingLevels: THINKING_EFFORTS,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
hook: (r, v) => {
|
|
189
|
+
r.hooks = [...(r.hooks ?? []), v as string];
|
|
190
|
+
},
|
|
191
|
+
extension: (r, v) => {
|
|
192
|
+
r.extensions = [...(r.extensions ?? []), v as string];
|
|
193
|
+
},
|
|
194
|
+
"plugin-dir": (r, v) => {
|
|
195
|
+
r.pluginDirs = [...(r.pluginDirs ?? []), v as string];
|
|
196
|
+
},
|
|
197
|
+
"no-extensions": r => {
|
|
198
|
+
r.noExtensions = true;
|
|
199
|
+
},
|
|
200
|
+
"no-skills": r => {
|
|
201
|
+
r.noSkills = true;
|
|
202
|
+
},
|
|
203
|
+
skills: (r, v) => {
|
|
204
|
+
r.skills = (v as string).split(",").map(s => s.trim());
|
|
205
|
+
},
|
|
206
|
+
"no-rules": r => {
|
|
207
|
+
r.noRules = true;
|
|
208
|
+
},
|
|
209
|
+
export: (r, v) => {
|
|
210
|
+
r.export = v as string;
|
|
211
|
+
},
|
|
212
|
+
"list-models": (r, v) => {
|
|
213
|
+
r.listModels = v;
|
|
214
|
+
},
|
|
215
|
+
"no-title": r => {
|
|
216
|
+
r.noTitle = true;
|
|
217
|
+
},
|
|
218
|
+
help: r => {
|
|
219
|
+
r.help = true;
|
|
220
|
+
},
|
|
221
|
+
version: r => {
|
|
222
|
+
r.version = true;
|
|
223
|
+
},
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/** True when the token could be an optional flag's value rather than the next flag or a file arg. */
|
|
227
|
+
function isValueToken(token: string | undefined): token is string {
|
|
228
|
+
return token !== undefined && !token.startsWith("-") && !token.startsWith("@");
|
|
58
229
|
}
|
|
59
230
|
|
|
60
231
|
export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "boolean" | "string" }>): Args {
|
|
@@ -62,146 +233,77 @@ export function parseArgs(args: string[], extensionFlags?: Map<string, { type: "
|
|
|
62
233
|
messages: [],
|
|
63
234
|
fileArgs: [],
|
|
64
235
|
unknownFlags: new Map(),
|
|
236
|
+
unrecognizedFlags: [],
|
|
65
237
|
};
|
|
66
238
|
|
|
67
|
-
|
|
68
|
-
const arg = args[i];
|
|
239
|
+
const tokens = normalizeFlagTokens(args, extensionFlags);
|
|
69
240
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
241
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
242
|
+
const token = tokens[i];
|
|
243
|
+
|
|
244
|
+
// Everything after `--` is content, not flags. Needed because an unrecognized flag is now a
|
|
245
|
+
// hard error, so there has to be a way to pass flag-looking text as a prompt.
|
|
246
|
+
if (token === "--") {
|
|
247
|
+
result.messages.push(...tokens.slice(i + 1));
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (token.startsWith("@")) {
|
|
252
|
+
result.fileArgs.push(token.slice(1));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!token.startsWith("-") || token === "-") {
|
|
257
|
+
result.messages.push(token);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// An unknown `--name=value` survives normalization intact, so strip the value to get a name
|
|
262
|
+
// that can still be matched against the extension flag registry.
|
|
263
|
+
const name = token.startsWith("--") ? token.slice(2).split("=")[0] : flagNameForChar(token.slice(1));
|
|
264
|
+
const spec = name === undefined ? undefined : flagSpec(name);
|
|
265
|
+
|
|
266
|
+
if (spec && name !== undefined) {
|
|
267
|
+
if (!takesValue(spec)) {
|
|
268
|
+
APPLY[name as LaunchFlagName](result, true);
|
|
269
|
+
continue;
|
|
94
270
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
result.provider = args[++i];
|
|
99
|
-
} else if (arg === "--model" && i + 1 < args.length) {
|
|
100
|
-
result.model = args[++i];
|
|
101
|
-
} else if (arg === "--smol" && i + 1 < args.length) {
|
|
102
|
-
result.smol = args[++i];
|
|
103
|
-
} else if (arg === "--slow" && i + 1 < args.length) {
|
|
104
|
-
result.slow = args[++i];
|
|
105
|
-
} else if (arg === "--plan" && i + 1 < args.length) {
|
|
106
|
-
result.plan = args[++i];
|
|
107
|
-
} else if (arg === "--api-key" && i + 1 < args.length) {
|
|
108
|
-
result.apiKey = args[++i];
|
|
109
|
-
} else if (arg === "--system-prompt" && i + 1 < args.length) {
|
|
110
|
-
result.systemPrompt = args[++i];
|
|
111
|
-
} else if (arg === "--append-system-prompt" && i + 1 < args.length) {
|
|
112
|
-
result.appendSystemPrompt = args[++i];
|
|
113
|
-
} else if (arg === "--provider-session-id" && i + 1 < args.length) {
|
|
114
|
-
result.providerSessionId = args[++i];
|
|
115
|
-
} else if (arg === "--no-session") {
|
|
116
|
-
result.noSession = true;
|
|
117
|
-
} else if (arg === "--session-dir" && i + 1 < args.length) {
|
|
118
|
-
result.sessionDir = args[++i];
|
|
119
|
-
} else if (arg === "--models" && i + 1 < args.length) {
|
|
120
|
-
result.models = args[++i].split(",").map(s => s.trim());
|
|
121
|
-
} else if (arg === "--no-tools") {
|
|
122
|
-
result.noTools = true;
|
|
123
|
-
} else if (arg === "--no-lsp") {
|
|
124
|
-
result.noLsp = true;
|
|
125
|
-
} else if (arg === "--no-pty") {
|
|
126
|
-
result.noPty = true;
|
|
127
|
-
} else if (arg === "--no-mcp") {
|
|
128
|
-
result.noMcp = true;
|
|
129
|
-
} else if (arg === "--tools" && i + 1 < args.length) {
|
|
130
|
-
const toolNames = args[++i]
|
|
131
|
-
.split(",")
|
|
132
|
-
.map(s => s.trim().toLowerCase())
|
|
133
|
-
.filter(Boolean);
|
|
134
|
-
const validTools: string[] = [];
|
|
135
|
-
for (const name of toolNames) {
|
|
136
|
-
if (name in BUILTIN_TOOLS) {
|
|
137
|
-
validTools.push(name);
|
|
138
|
-
} else {
|
|
139
|
-
logger.warn("Unknown tool passed to --tools", {
|
|
140
|
-
tool: name,
|
|
141
|
-
validTools: Object.keys(BUILTIN_TOOLS),
|
|
142
|
-
});
|
|
143
|
-
}
|
|
271
|
+
if (spec.arity === "optional-value") {
|
|
272
|
+
APPLY[name as LaunchFlagName](result, isValueToken(tokens[i + 1]) ? tokens[++i] : true);
|
|
273
|
+
continue;
|
|
144
274
|
}
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
if (thinking !== undefined) {
|
|
150
|
-
result.thinking = thinking;
|
|
151
|
-
} else {
|
|
152
|
-
logger.warn("Invalid thinking level passed to --thinking", {
|
|
153
|
-
level: rawThinking,
|
|
154
|
-
validThinkingLevels: THINKING_EFFORTS,
|
|
155
|
-
});
|
|
275
|
+
const value = tokens[i + 1];
|
|
276
|
+
if (value === undefined) {
|
|
277
|
+
result.unrecognizedFlags.push({ token, name });
|
|
278
|
+
continue;
|
|
156
279
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
} else if (arg === "--hook" && i + 1 < args.length) {
|
|
162
|
-
result.hooks = result.hooks ?? [];
|
|
163
|
-
result.hooks.push(args[++i]);
|
|
164
|
-
} else if ((arg === "--extension" || arg === "-e") && i + 1 < args.length) {
|
|
165
|
-
result.extensions = result.extensions ?? [];
|
|
166
|
-
result.extensions.push(args[++i]);
|
|
167
|
-
} else if (arg === "--plugin-dir" && i + 1 < args.length) {
|
|
168
|
-
result.pluginDirs = result.pluginDirs ?? [];
|
|
169
|
-
result.pluginDirs.push(args[++i]);
|
|
170
|
-
} else if (arg === "--no-extensions") {
|
|
171
|
-
result.noExtensions = true;
|
|
172
|
-
} else if (arg === "--no-skills") {
|
|
173
|
-
result.noSkills = true;
|
|
174
|
-
} else if (arg === "--no-rules") {
|
|
175
|
-
result.noRules = true;
|
|
176
|
-
} else if (arg === "--no-title") {
|
|
177
|
-
result.noTitle = true;
|
|
178
|
-
} else if (arg === "--skills" && i + 1 < args.length) {
|
|
179
|
-
// Comma-separated glob patterns for skill filtering
|
|
180
|
-
result.skills = args[++i].split(",").map(s => s.trim());
|
|
181
|
-
} else if (arg === "--list-models") {
|
|
182
|
-
// Check if next arg is a search pattern (not a flag or file arg)
|
|
183
|
-
if (i + 1 < args.length && !args[i + 1].startsWith("-") && !args[i + 1].startsWith("@")) {
|
|
184
|
-
result.listModels = args[++i];
|
|
185
|
-
} else {
|
|
186
|
-
result.listModels = true;
|
|
280
|
+
i++;
|
|
281
|
+
if (spec.options && !spec.options.includes(value)) {
|
|
282
|
+
logger.warn("Invalid value passed to flag", { flag: token, value, validValues: spec.options });
|
|
283
|
+
continue;
|
|
187
284
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
}
|
|
285
|
+
APPLY[name as LaunchFlagName](result, value);
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Extension flags are only known on the second parse, once extensions have loaded.
|
|
290
|
+
const extFlag = name === undefined ? undefined : extensionFlags?.get(name);
|
|
291
|
+
if (extFlag && name !== undefined) {
|
|
292
|
+
if (extFlag.type === "boolean") {
|
|
293
|
+
result.unknownFlags.set(name, true);
|
|
294
|
+
} else if (i + 1 < tokens.length) {
|
|
295
|
+
result.unknownFlags.set(name, tokens[++i]);
|
|
200
296
|
}
|
|
201
|
-
|
|
202
|
-
} else if (!arg.startsWith("-")) {
|
|
203
|
-
result.messages.push(arg);
|
|
297
|
+
continue;
|
|
204
298
|
}
|
|
299
|
+
|
|
300
|
+
// Record the flag, but do NOT consume the token after it. The bootstrap parse runs before
|
|
301
|
+
// extensions load, so it cannot know whether an unrecognized flag takes a value: swallowing
|
|
302
|
+
// the next token silently discards the user's prompt whenever the flag turns out to be
|
|
303
|
+
// boolean (`xcsh -p --verbose "do work"`). Leaving it means a string extension flag's value
|
|
304
|
+
// still reaches `messages`, which is the pre-existing behaviour and the lesser harm — the
|
|
305
|
+
// real fix is to load extensions before the first parse, which is out of scope here.
|
|
306
|
+
result.unrecognizedFlags.push({ token, name: name ?? token.replace(/^-+/, "") });
|
|
205
307
|
}
|
|
206
308
|
|
|
207
309
|
return result;
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single source of truth for the root command's long flags.
|
|
3
|
+
*
|
|
4
|
+
* `xcsh --help` documented every long flag in the `=<value>` form while the parser matched only
|
|
5
|
+
* `--flag value`, so `xcsh --model=opus` silently ran the configured default and
|
|
6
|
+
* `xcsh --list-models=claude` fell through to the interactive TUI and hung (#2469). The two lived in
|
|
7
|
+
* separate tables — `commands/launch.ts` for help, `cli/args.ts` for parsing — and drifted.
|
|
8
|
+
*
|
|
9
|
+
* Both now derive from `LAUNCH_FLAGS`: `buildCliFlags` renders help from it, and `parseArgs` consumes
|
|
10
|
+
* it to resolve names and arity. Help and parser cannot disagree because they read the same datum.
|
|
11
|
+
*
|
|
12
|
+
* A leaf module on purpose — it imports only the thinking-effort list, so `commands/launch.ts` and
|
|
13
|
+
* `cli/args.ts` can both depend on it without a cycle.
|
|
14
|
+
*/
|
|
15
|
+
import { THINKING_EFFORTS } from "@f5-sales-demo/pi-ai";
|
|
16
|
+
import { type FlagDescriptor, Flags } from "@f5-sales-demo/pi-utils/cli";
|
|
17
|
+
|
|
18
|
+
export type FlagArity = "boolean" | "value" | "optional-value" | "repeatable-value";
|
|
19
|
+
|
|
20
|
+
export interface FlagSpec {
|
|
21
|
+
arity: FlagArity;
|
|
22
|
+
/** Single-character alias, as `-p` for `--print`. */
|
|
23
|
+
char?: string;
|
|
24
|
+
description: string;
|
|
25
|
+
/** Enumerated legal values, validated by the parser and rendered into help. */
|
|
26
|
+
options?: readonly string[];
|
|
27
|
+
/** Accepted by the parser but omitted from help — aliases and internal plumbing. */
|
|
28
|
+
hidden?: true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Keeps the key names as literals (so `APPLY` must cover every one) while widening each value. */
|
|
32
|
+
const defineFlags = <T extends Record<string, FlagSpec>>(flags: T): { [K in keyof T]: FlagSpec } => flags;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Every long flag the root command accepts.
|
|
36
|
+
*
|
|
37
|
+
* Where the old tables disagreed, the parser's behavior won, since that is what users observed:
|
|
38
|
+
* `--resume` and `--list-models` take an optional value, and `--mode` accepts `acp`.
|
|
39
|
+
*/
|
|
40
|
+
export const LAUNCH_FLAGS = defineFlags({
|
|
41
|
+
model: {
|
|
42
|
+
arity: "value",
|
|
43
|
+
description: 'Model to use (fuzzy match: "opus", "gpt-5.2", or "p-openai/gpt-5.2")',
|
|
44
|
+
},
|
|
45
|
+
smol: { arity: "value", description: "Smol/fast model for lightweight tasks (or PI_SMOL_MODEL env)" },
|
|
46
|
+
slow: { arity: "value", description: "Slow/reasoning model for thorough analysis (or PI_SLOW_MODEL env)" },
|
|
47
|
+
plan: { arity: "value", description: "Plan model for architectural planning (or PI_PLAN_MODEL env)" },
|
|
48
|
+
provider: { arity: "value", description: "Provider to use (legacy; prefer --model)" },
|
|
49
|
+
"api-key": { arity: "value", description: "API key (defaults to env vars)" },
|
|
50
|
+
"system-prompt": { arity: "value", description: "System prompt (default: coding assistant prompt)" },
|
|
51
|
+
"append-system-prompt": { arity: "value", description: "Append text or file contents to the system prompt" },
|
|
52
|
+
"allow-home": { arity: "boolean", description: "Allow starting in ~ without auto-switching to a temp dir" },
|
|
53
|
+
"no-sandbox": {
|
|
54
|
+
arity: "boolean",
|
|
55
|
+
description: "Disable session filesystem isolation (allow access outside the CWD)",
|
|
56
|
+
},
|
|
57
|
+
"allow-path": {
|
|
58
|
+
arity: "repeatable-value",
|
|
59
|
+
description: "Grant read+write access to an extra directory (repeatable)",
|
|
60
|
+
},
|
|
61
|
+
mode: {
|
|
62
|
+
arity: "value",
|
|
63
|
+
description: "Output mode: text (default), json, rpc, or acp",
|
|
64
|
+
options: ["text", "json", "rpc", "acp"],
|
|
65
|
+
},
|
|
66
|
+
print: { arity: "boolean", char: "p", description: "Non-interactive mode: process prompt and exit" },
|
|
67
|
+
continue: { arity: "boolean", char: "c", description: "Continue previous session" },
|
|
68
|
+
resume: {
|
|
69
|
+
arity: "optional-value",
|
|
70
|
+
char: "r",
|
|
71
|
+
description: "Resume a session (by ID prefix, path, or picker if omitted)",
|
|
72
|
+
},
|
|
73
|
+
session: { arity: "optional-value", description: "Alias of --resume", hidden: true },
|
|
74
|
+
fork: { arity: "value", description: "Fork an existing session by ID prefix or path" },
|
|
75
|
+
"session-dir": { arity: "value", description: "Directory for session storage and lookup" },
|
|
76
|
+
"no-session": { arity: "boolean", description: "Don't save session (ephemeral)" },
|
|
77
|
+
"provider-session-id": { arity: "value", description: "Resume a provider-side session", hidden: true },
|
|
78
|
+
models: { arity: "value", description: "Comma-separated model patterns for Ctrl+P cycling" },
|
|
79
|
+
"no-tools": { arity: "boolean", description: "Disable all built-in tools" },
|
|
80
|
+
"no-mcp": { arity: "boolean", description: "Disable MCP server discovery and tools" },
|
|
81
|
+
"no-lsp": { arity: "boolean", description: "Disable LSP tools, formatting, and diagnostics" },
|
|
82
|
+
"no-pty": { arity: "boolean", description: "Disable PTY-based interactive bash execution" },
|
|
83
|
+
tools: { arity: "value", description: "Comma-separated list of tools to enable (default: all)" },
|
|
84
|
+
thinking: {
|
|
85
|
+
arity: "value",
|
|
86
|
+
description: `Set thinking level: ${THINKING_EFFORTS.join(", ")}`,
|
|
87
|
+
options: [...THINKING_EFFORTS],
|
|
88
|
+
},
|
|
89
|
+
hook: { arity: "repeatable-value", description: "Load a hook/extension file (can be used multiple times)" },
|
|
90
|
+
extension: {
|
|
91
|
+
arity: "repeatable-value",
|
|
92
|
+
char: "e",
|
|
93
|
+
description: "Load an extension file (can be used multiple times)",
|
|
94
|
+
},
|
|
95
|
+
"plugin-dir": { arity: "repeatable-value", description: "Load plugin from directory (repeatable)" },
|
|
96
|
+
"no-extensions": {
|
|
97
|
+
arity: "boolean",
|
|
98
|
+
description: "Disable extension discovery (explicit -e paths still work)",
|
|
99
|
+
},
|
|
100
|
+
"no-skills": { arity: "boolean", description: "Disable skills discovery and loading" },
|
|
101
|
+
skills: {
|
|
102
|
+
arity: "value",
|
|
103
|
+
description: "Comma-separated glob patterns to filter skills (e.g., git-*,docker)",
|
|
104
|
+
},
|
|
105
|
+
"no-rules": { arity: "boolean", description: "Disable rules discovery and loading" },
|
|
106
|
+
export: { arity: "value", description: "Export session file to HTML and exit" },
|
|
107
|
+
"list-models": { arity: "optional-value", description: "List available models (with optional fuzzy search)" },
|
|
108
|
+
"no-title": { arity: "boolean", description: "Disable title auto-generation" },
|
|
109
|
+
help: { arity: "boolean", char: "h", description: "Show help", hidden: true },
|
|
110
|
+
version: { arity: "boolean", char: "v", description: "Show version", hidden: true },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export type LaunchFlagName = keyof typeof LAUNCH_FLAGS;
|
|
114
|
+
|
|
115
|
+
/** Long-flag name for a single-character alias, e.g. `p` -> `print`. */
|
|
116
|
+
const CHAR_ALIASES: ReadonlyMap<string, LaunchFlagName> = new Map(
|
|
117
|
+
Object.entries(LAUNCH_FLAGS)
|
|
118
|
+
.filter((entry): entry is [LaunchFlagName, FlagSpec & { char: string }] => "char" in entry[1])
|
|
119
|
+
.map(([name, spec]) => [spec.char, name]),
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
export function flagSpec(name: string): FlagSpec | undefined {
|
|
123
|
+
return (LAUNCH_FLAGS as Record<string, FlagSpec>)[name];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function flagNameForChar(char: string): LaunchFlagName | undefined {
|
|
127
|
+
return CHAR_ALIASES.get(char);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function takesValue(spec: FlagSpec): boolean {
|
|
131
|
+
return spec.arity !== "boolean";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Render the help table from the spec, so the documented syntax is the accepted syntax. */
|
|
135
|
+
export function buildCliFlags(): Record<string, FlagDescriptor> {
|
|
136
|
+
const flags: Record<string, FlagDescriptor> = {};
|
|
137
|
+
for (const [name, spec] of Object.entries(LAUNCH_FLAGS) as Array<[string, FlagSpec]>) {
|
|
138
|
+
if (spec.hidden) continue;
|
|
139
|
+
flags[name] =
|
|
140
|
+
spec.arity === "boolean"
|
|
141
|
+
? Flags.boolean({ char: spec.char, description: spec.description })
|
|
142
|
+
: Flags.string({
|
|
143
|
+
char: spec.char,
|
|
144
|
+
description: spec.description,
|
|
145
|
+
options: spec.options,
|
|
146
|
+
multiple: spec.arity === "repeatable-value",
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return flags;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** A `--name=value` token the parser could not attribute to any known flag. */
|
|
153
|
+
export interface UnrecognizedFlag {
|
|
154
|
+
/** The token as written, including any `=value`, for the diagnostic. */
|
|
155
|
+
token: string;
|
|
156
|
+
/** The flag name without dashes or `=value`, for matching against extension flags. */
|
|
157
|
+
name: string;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export class FlagUsageError extends Error {}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Rewrite `--name=value` into `["--name", "value"]` for every flag that takes a value.
|
|
164
|
+
*
|
|
165
|
+
* A boolean flag with `=` is an error rather than a guess: accepting `--no-sandbox=true` invites
|
|
166
|
+
* `--no-sandbox=false`, which the parser has no way to express, and quietly reading it as "on" would
|
|
167
|
+
* be exactly the class of bug #2469 reports. Unknown names are left intact so the unknown-flag path
|
|
168
|
+
* can report the token as the user wrote it.
|
|
169
|
+
*
|
|
170
|
+
* Short forms are untouched: no shell convention makes `-p=x` mean `-p x`.
|
|
171
|
+
*/
|
|
172
|
+
export function normalizeFlagTokens(
|
|
173
|
+
args: readonly string[],
|
|
174
|
+
extensionFlags?: ReadonlyMap<string, { type: "boolean" | "string" }>,
|
|
175
|
+
): string[] {
|
|
176
|
+
const normalized: string[] = [];
|
|
177
|
+
let afterTerminator = false;
|
|
178
|
+
|
|
179
|
+
for (const arg of args) {
|
|
180
|
+
if (afterTerminator) {
|
|
181
|
+
normalized.push(arg);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (arg === "--") {
|
|
185
|
+
afterTerminator = true;
|
|
186
|
+
normalized.push(arg);
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const match = /^--([^=]+)=([\s\S]*)$/.exec(arg);
|
|
191
|
+
if (!match) {
|
|
192
|
+
normalized.push(arg);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const [, name, value] = match;
|
|
197
|
+
const spec = flagSpec(name);
|
|
198
|
+
if (spec) {
|
|
199
|
+
if (!takesValue(spec)) {
|
|
200
|
+
throw new FlagUsageError(`--${name} is a boolean flag and does not take a value`);
|
|
201
|
+
}
|
|
202
|
+
normalized.push(`--${name}`, value);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const extension = extensionFlags?.get(name);
|
|
207
|
+
if (extension) {
|
|
208
|
+
if (extension.type === "boolean") {
|
|
209
|
+
throw new FlagUsageError(`--${name} is a boolean flag and does not take a value`);
|
|
210
|
+
}
|
|
211
|
+
normalized.push(`--${name}`, value);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
normalized.push(arg);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return normalized;
|
|
219
|
+
}
|
package/src/cli/office-cli.ts
CHANGED
|
@@ -222,11 +222,25 @@ async function runSideload(app: OfficeApp): Promise<void> {
|
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
225
|
+
/**
|
|
226
|
+
* The two long-running halves of the command, injectable so the dispatch order can be
|
|
227
|
+
* tested without binding :8444 or shelling out to `office-addin-debugging`.
|
|
228
|
+
*/
|
|
229
|
+
export interface OfficeCommandDeps {
|
|
230
|
+
sideload: (app: OfficeApp) => Promise<void>;
|
|
231
|
+
serve: () => Promise<void>;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const defaultCommandDeps: OfficeCommandDeps = { sideload: runSideload, serve: runServe };
|
|
235
|
+
|
|
225
236
|
/** Dispatch an `xcsh office <action>` invocation. */
|
|
226
|
-
export async function runOfficeCommand(
|
|
237
|
+
export async function runOfficeCommand(
|
|
238
|
+
args: OfficeCommandArgs,
|
|
239
|
+
deps: OfficeCommandDeps = defaultCommandDeps,
|
|
240
|
+
): Promise<void> {
|
|
227
241
|
switch (args.action) {
|
|
228
242
|
case "serve":
|
|
229
|
-
await
|
|
243
|
+
await deps.serve();
|
|
230
244
|
return;
|
|
231
245
|
case "manifest": {
|
|
232
246
|
const text = await writeManifest(args.out);
|
|
@@ -238,7 +252,16 @@ export async function runOfficeCommand(args: OfficeCommandArgs): Promise<void> {
|
|
|
238
252
|
return;
|
|
239
253
|
}
|
|
240
254
|
case "sideload":
|
|
241
|
-
|
|
255
|
+
// Register FIRST, then serve — serve blocks until a signal, so anything
|
|
256
|
+
// sequenced after it would never run.
|
|
257
|
+
//
|
|
258
|
+
// Registering alone used to be the whole command, which quietly left the
|
|
259
|
+
// operator half-configured: the pane's cwd (what its file tools and shell are
|
|
260
|
+
// confined to) comes from wherever `office serve` was launched, so a bare
|
|
261
|
+
// sideload produced a pane pointed at some unrelated folder, or at nothing.
|
|
262
|
+
// One command from the folder you care about now does both.
|
|
263
|
+
await deps.sideload(args.app ?? "excel");
|
|
264
|
+
await deps.serve();
|
|
242
265
|
return;
|
|
243
266
|
case "recycle":
|
|
244
267
|
console.log(await recycleOfficeServe());
|