@oh-my-pi/pi-coding-agent 15.6.0 → 15.7.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 (140) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/dist/types/capability/rule-buckets.d.ts +30 -0
  3. package/dist/types/capability/rule.d.ts +7 -0
  4. package/dist/types/cli/completion-gen.d.ts +80 -0
  5. package/dist/types/commands/complete.d.ts +6 -0
  6. package/dist/types/commands/completions.d.ts +13 -0
  7. package/dist/types/commands/setup.d.ts +10 -1
  8. package/dist/types/config/settings-schema.d.ts +170 -10
  9. package/dist/types/discovery/builtin-defaults.d.ts +1 -0
  10. package/dist/types/discovery/builtin-rules/index.d.ts +7 -0
  11. package/dist/types/discovery/index.d.ts +1 -0
  12. package/dist/types/edit/hashline/block-resolver.d.ts +9 -0
  13. package/dist/types/edit/hashline/index.d.ts +1 -0
  14. package/dist/types/eval/py/kernel.d.ts +3 -0
  15. package/dist/types/eval/py/runtime.d.ts +11 -1
  16. package/dist/types/export/html/template.generated.d.ts +1 -1
  17. package/dist/types/main.d.ts +1 -0
  18. package/dist/types/modes/components/index.d.ts +1 -0
  19. package/dist/types/modes/components/segment-track.d.ts +22 -0
  20. package/dist/types/modes/components/welcome.d.ts +21 -0
  21. package/dist/types/modes/interactive-mode.d.ts +3 -2
  22. package/dist/types/modes/setup-wizard/index.d.ts +16 -0
  23. package/dist/types/modes/setup-wizard/scenes/glyph.d.ts +2 -0
  24. package/dist/types/modes/setup-wizard/scenes/outro.d.ts +2 -0
  25. package/dist/types/modes/setup-wizard/scenes/providers.d.ts +2 -0
  26. package/dist/types/modes/setup-wizard/scenes/sign-in.d.ts +19 -0
  27. package/dist/types/modes/setup-wizard/scenes/splash.d.ts +11 -0
  28. package/dist/types/modes/setup-wizard/scenes/theme.d.ts +2 -0
  29. package/dist/types/modes/setup-wizard/scenes/types.d.ts +43 -0
  30. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +19 -0
  31. package/dist/types/modes/setup-wizard/wizard-overlay.d.ts +14 -0
  32. package/dist/types/modes/theme/shimmer.d.ts +2 -0
  33. package/dist/types/modes/theme/theme.d.ts +11 -0
  34. package/dist/types/modes/types.d.ts +5 -1
  35. package/dist/types/tiny/device.d.ts +78 -0
  36. package/dist/types/tiny/dtype.d.ts +85 -0
  37. package/dist/types/tiny/models.d.ts +6 -6
  38. package/dist/types/tiny/text.d.ts +15 -0
  39. package/dist/types/tiny/title-client.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +0 -1
  41. package/dist/types/tools/eval.d.ts +1 -1
  42. package/dist/types/tools/index.d.ts +0 -1
  43. package/dist/types/tui/code-cell.d.ts +2 -0
  44. package/dist/types/tui/output-block.d.ts +17 -0
  45. package/package.json +9 -9
  46. package/src/capability/rule-buckets.ts +64 -0
  47. package/src/capability/rule.ts +8 -0
  48. package/src/cli/completion-gen.ts +550 -0
  49. package/src/cli/setup-cli.ts +5 -3
  50. package/src/cli-commands.ts +2 -0
  51. package/src/cli.ts +1 -7
  52. package/src/commands/complete.ts +66 -0
  53. package/src/commands/completions.ts +60 -0
  54. package/src/commands/setup.ts +29 -4
  55. package/src/config/settings-schema.ts +70 -11
  56. package/src/discovery/builtin-defaults.ts +39 -0
  57. package/src/discovery/builtin-rules/index.ts +48 -0
  58. package/src/discovery/builtin-rules/rs-box-leak.md +48 -0
  59. package/src/discovery/builtin-rules/rs-future-prelude.md +23 -0
  60. package/src/discovery/builtin-rules/rs-lazylock.md +51 -0
  61. package/src/discovery/builtin-rules/rs-match-ergonomics.md +67 -0
  62. package/src/discovery/builtin-rules/rs-parking-lot.md +44 -0
  63. package/src/discovery/builtin-rules/rs-result-type.md +19 -0
  64. package/src/discovery/builtin-rules/ts-bare-catch.md +38 -0
  65. package/src/discovery/builtin-rules/ts-import-type.md +42 -0
  66. package/src/discovery/builtin-rules/ts-no-any.md +56 -0
  67. package/src/discovery/builtin-rules/ts-no-dynamic-import.md +39 -0
  68. package/src/discovery/builtin-rules/ts-no-return-type.md +45 -0
  69. package/src/discovery/builtin-rules/ts-no-tiny-functions.md +50 -0
  70. package/src/discovery/builtin-rules/ts-promise-with-resolvers.md +65 -0
  71. package/src/discovery/builtin-rules/ts-set-map.md +28 -0
  72. package/src/discovery/index.ts +1 -0
  73. package/src/edit/hashline/block-resolver.ts +14 -0
  74. package/src/edit/hashline/diff.ts +4 -1
  75. package/src/edit/hashline/execute.ts +2 -1
  76. package/src/edit/hashline/index.ts +1 -0
  77. package/src/eval/py/kernel.ts +37 -15
  78. package/src/eval/py/runtime.ts +57 -28
  79. package/src/export/html/template.generated.ts +1 -1
  80. package/src/export/html/template.js +0 -12
  81. package/src/export/ttsr.ts +2 -0
  82. package/src/internal-urls/docs-index.generated.ts +7 -8
  83. package/src/main.ts +18 -1
  84. package/src/modes/components/hook-selector.ts +15 -17
  85. package/src/modes/components/index.ts +1 -0
  86. package/src/modes/components/segment-track.ts +52 -0
  87. package/src/modes/components/tips.txt +2 -1
  88. package/src/modes/components/tool-execution.ts +5 -1
  89. package/src/modes/components/welcome.ts +47 -42
  90. package/src/modes/controllers/input-controller.ts +12 -21
  91. package/src/modes/interactive-mode.ts +17 -5
  92. package/src/modes/setup-wizard/index.ts +88 -0
  93. package/src/modes/setup-wizard/scenes/glyph.ts +96 -0
  94. package/src/modes/setup-wizard/scenes/outro.ts +35 -0
  95. package/src/modes/setup-wizard/scenes/providers.ts +69 -0
  96. package/src/modes/setup-wizard/scenes/sign-in.ts +193 -0
  97. package/src/modes/setup-wizard/scenes/splash.ts +201 -0
  98. package/src/modes/setup-wizard/scenes/theme.ts +299 -0
  99. package/src/modes/setup-wizard/scenes/types.ts +48 -0
  100. package/src/modes/setup-wizard/scenes/web-search.ts +128 -0
  101. package/src/modes/setup-wizard/wizard-overlay.ts +275 -0
  102. package/src/modes/theme/shimmer.ts +5 -0
  103. package/src/modes/theme/theme.ts +44 -20
  104. package/src/modes/types.ts +6 -1
  105. package/src/prompts/system/orchestrate-notice.md +1 -1
  106. package/src/prompts/tools/read.md +4 -0
  107. package/src/sdk.ts +5 -15
  108. package/src/slash-commands/builtin-registry.ts +8 -0
  109. package/src/tiny/device.ts +117 -0
  110. package/src/tiny/dtype.ts +101 -0
  111. package/src/tiny/models.ts +7 -6
  112. package/src/tiny/text.ts +36 -1
  113. package/src/tiny/title-client.ts +58 -3
  114. package/src/tiny/worker.ts +93 -29
  115. package/src/tools/bash.ts +16 -13
  116. package/src/tools/eval.ts +9 -4
  117. package/src/tools/index.ts +0 -11
  118. package/src/tools/read.ts +1 -0
  119. package/src/tools/renderers.ts +0 -2
  120. package/src/tui/code-cell.ts +6 -1
  121. package/src/tui/output-block.ts +199 -38
  122. package/dist/types/tools/recipe/index.d.ts +0 -46
  123. package/dist/types/tools/recipe/render.d.ts +0 -36
  124. package/dist/types/tools/recipe/runner.d.ts +0 -60
  125. package/dist/types/tools/recipe/runners/cargo.d.ts +0 -16
  126. package/dist/types/tools/recipe/runners/index.d.ts +0 -2
  127. package/dist/types/tools/recipe/runners/just.d.ts +0 -2
  128. package/dist/types/tools/recipe/runners/make.d.ts +0 -2
  129. package/dist/types/tools/recipe/runners/pkg.d.ts +0 -2
  130. package/dist/types/tools/recipe/runners/task.d.ts +0 -2
  131. package/src/prompts/tools/recipe.md +0 -16
  132. package/src/tools/recipe/index.ts +0 -81
  133. package/src/tools/recipe/render.ts +0 -19
  134. package/src/tools/recipe/runner.ts +0 -219
  135. package/src/tools/recipe/runners/cargo.ts +0 -131
  136. package/src/tools/recipe/runners/index.ts +0 -8
  137. package/src/tools/recipe/runners/just.ts +0 -73
  138. package/src/tools/recipe/runners/make.ts +0 -101
  139. package/src/tools/recipe/runners/pkg.ts +0 -167
  140. package/src/tools/recipe/runners/task.ts +0 -72
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Rule bucketing
3
+ *
4
+ * Single funnel that every discovered rule passes through on its way into a
5
+ * session. It applies the user's disable levers, registers TTSR rules with the
6
+ * manager, and splits the rest into the always-apply and rulebook buckets.
7
+ *
8
+ * Bucket precedence (matches docs/rulebook-matching-pipeline.md §5):
9
+ * 1. TTSR — non-empty `condition` that `TtsrManager.addRule` accepts
10
+ * 2. always — `alwaysApply === true`
11
+ * 3. rulebook — has a `description`
12
+ */
13
+ import type { TtsrManager } from "../export/ttsr";
14
+ import { BUILTIN_DEFAULTS_PROVIDER_ID, type Rule } from "./rule";
15
+
16
+ export interface RuleBuckets {
17
+ rulebookRules: Rule[];
18
+ alwaysApplyRules: Rule[];
19
+ }
20
+
21
+ export interface BucketRulesOptions {
22
+ /** Rule names to drop entirely (bundled defaults and user rules alike). */
23
+ disabledRules?: readonly string[];
24
+ /** When false, drop every rule from the bundled `builtin-defaults` provider. */
25
+ builtinRules?: boolean;
26
+ }
27
+
28
+ /**
29
+ * Filter and bucket rules, registering TTSR rules on `ttsrManager` as a side
30
+ * effect. Disabled rules are dropped before any bucket assignment, so a
31
+ * disabled rule is neither matched as TTSR nor surfaced via `rule://`.
32
+ */
33
+ export function bucketRules(
34
+ rules: readonly Rule[],
35
+ ttsrManager: TtsrManager,
36
+ options: BucketRulesOptions = {},
37
+ ): RuleBuckets {
38
+ const includeBuiltin = options.builtinRules !== false;
39
+ const disabled = new Set<string>();
40
+ for (const raw of options.disabledRules ?? []) {
41
+ const name = raw.trim();
42
+ if (name.length > 0) disabled.add(name);
43
+ }
44
+
45
+ const rulebookRules: Rule[] = [];
46
+ const alwaysApplyRules: Rule[] = [];
47
+
48
+ for (const rule of rules) {
49
+ if (disabled.has(rule.name)) continue;
50
+ if (!includeBuiltin && rule._source?.provider === BUILTIN_DEFAULTS_PROVIDER_ID) continue;
51
+
52
+ const isTtsrRule = rule.condition && rule.condition.length > 0 ? ttsrManager.addRule(rule) : false;
53
+ if (isTtsrRule) continue;
54
+ if (rule.alwaysApply === true) {
55
+ alwaysApplyRules.push(rule);
56
+ continue;
57
+ }
58
+ if (rule.description) {
59
+ rulebookRules.push(rule);
60
+ }
61
+ }
62
+
63
+ return { rulebookRules, alwaysApplyRules };
64
+ }
@@ -9,6 +9,14 @@ import type { SourceMeta } from "./types";
9
9
 
10
10
  const CONDITION_GLOB_SCOPE_TOOLS = ["edit", "write"] as const;
11
11
 
12
+ /**
13
+ * Provider id for the bundled default rules shipped with the agent.
14
+ * Lowest priority, so any user/project/tool rule of the same name overrides
15
+ * a bundled default. Also used to gate the whole bundled set via
16
+ * `ttsr.builtinRules`.
17
+ */
18
+ export const BUILTIN_DEFAULTS_PROVIDER_ID = "builtin-defaults";
19
+
12
20
  /**
13
21
  * Parsed frontmatter from rule files.
14
22
  */
@@ -0,0 +1,550 @@
1
+ /**
2
+ * Shell-completion generation (bash, zsh, fish).
3
+ *
4
+ * Single source of truth: the declarative `flags`/`args` descriptors carried by
5
+ * each `Command` subclass plus the registered subcommand table. {@link buildSpec}
6
+ * walks that metadata — the same data `renderCommandBody` renders for `--help` —
7
+ * and {@link generateCompletion} emits a self-contained completion script. Adding
8
+ * a flag to a command's static `flags` therefore propagates into completions with
9
+ * no edits here.
10
+ *
11
+ * Static candidates (enum `options`, the builtin tool list) are baked into the
12
+ * script. A small set of flags resolve dynamic candidates (the live model
13
+ * catalog and on-disk sessions) by calling back into `<bin> __complete <kind>`
14
+ * — see `commands/complete.ts`. The flag→source mapping below is the only manual
15
+ * knob and is keyed by flag name so it stays stable as flags are added.
16
+ */
17
+ import type { ArgDescriptor, CliConfig, CommandCtor, FlagDescriptor } from "@oh-my-pi/pi-utils/cli";
18
+ import { BUILTIN_TOOLS } from "../tools";
19
+
20
+ export type Shell = "bash" | "zsh" | "fish";
21
+
22
+ /** How a flag/positional value should be completed. */
23
+ export type ValueSource =
24
+ | { kind: "flag" } // boolean — takes no value
25
+ | { kind: "value" } // takes a value with no completable candidates (e.g. integer, free text)
26
+ | { kind: "enum"; values: readonly string[] } // static single value
27
+ | { kind: "list"; values: readonly string[] } // static comma-separated list
28
+ | { kind: "models"; multiple: boolean } // dynamic: live model catalog
29
+ | { kind: "sessions" } // dynamic: on-disk sessions
30
+ | { kind: "file" }
31
+ | { kind: "dir" };
32
+
33
+ export interface CompletionFlag {
34
+ /** Long name without the leading `--`. */
35
+ name: string;
36
+ /** Short character without the leading `-`. */
37
+ char?: string;
38
+ description: string;
39
+ value: ValueSource;
40
+ /** Flag may appear multiple times (oclif `multiple`). */
41
+ repeatable: boolean;
42
+ }
43
+
44
+ export interface CompletionArg {
45
+ name: string;
46
+ description: string;
47
+ value: ValueSource;
48
+ }
49
+
50
+ export interface CompletionCommand {
51
+ name: string;
52
+ aliases: readonly string[];
53
+ description: string;
54
+ flags: CompletionFlag[];
55
+ args: CompletionArg[];
56
+ }
57
+
58
+ export interface CompletionSpec {
59
+ bin: string;
60
+ /** Flags/args of the default (no-subcommand) command. */
61
+ root: { flags: CompletionFlag[]; args: CompletionArg[] };
62
+ commands: CompletionCommand[];
63
+ }
64
+
65
+ // --- Flag/arg value classification (the single manual mapping) ----------------
66
+
67
+ /** Single-value flags resolved against the live model catalog. */
68
+ const MODEL_FLAGS: Record<string, true> = { model: true, smol: true, slow: true, plan: true };
69
+ /** Single-value flags resolved against on-disk sessions. */
70
+ const SESSION_FLAGS: Record<string, true> = { resume: true, fork: true, session: true };
71
+ /** Flags whose value is a directory path. */
72
+ const DIR_FLAGS: Record<string, true> = { "session-dir": true, "plugin-dir": true };
73
+
74
+ function flagValue(name: string, desc: FlagDescriptor): ValueSource {
75
+ if (desc.kind === "boolean") return { kind: "flag" };
76
+ if (desc.options && desc.options.length > 0) return { kind: "enum", values: desc.options };
77
+ if (MODEL_FLAGS[name]) return { kind: "models", multiple: false };
78
+ if (name === "models") return { kind: "models", multiple: true };
79
+ if (SESSION_FLAGS[name]) return { kind: "sessions" };
80
+ if (name === "tools") return { kind: "list", values: Object.keys(BUILTIN_TOOLS) };
81
+ if (DIR_FLAGS[name]) return { kind: "dir" };
82
+ if (desc.kind === "integer") return { kind: "value" };
83
+ return { kind: "file" };
84
+ }
85
+
86
+ function argValue(desc: ArgDescriptor): ValueSource {
87
+ if (desc.options && desc.options.length > 0) return { kind: "enum", values: desc.options };
88
+ return { kind: "file" };
89
+ }
90
+
91
+ function buildFlags(Cmd: CommandCtor): CompletionFlag[] {
92
+ const out: CompletionFlag[] = [];
93
+ const flags = Cmd.flags ?? {};
94
+ for (const name in flags) {
95
+ const desc = flags[name];
96
+ out.push({
97
+ name,
98
+ char: desc.char,
99
+ description: desc.description ?? "",
100
+ value: flagValue(name, desc),
101
+ repeatable: Boolean(desc.multiple),
102
+ });
103
+ }
104
+ return out;
105
+ }
106
+
107
+ function buildArgs(Cmd: CommandCtor): CompletionArg[] {
108
+ const out: CompletionArg[] = [];
109
+ const args = Cmd.args ?? {};
110
+ for (const name in args) {
111
+ const desc = args[name];
112
+ out.push({ name, description: desc.description ?? "", value: argValue(desc) });
113
+ }
114
+ return out;
115
+ }
116
+
117
+ /**
118
+ * Build a {@link CompletionSpec} from loaded command classes.
119
+ *
120
+ * @param rootName Entry name of the default command (its flags become top-level
121
+ * flags; it is excluded from the subcommand list).
122
+ * @param aliasMap Canonical-name → aliases (merged from the registration table
123
+ * and the command class's static `aliases`).
124
+ */
125
+ export function buildSpec(
126
+ config: CliConfig,
127
+ rootName: string,
128
+ aliasMap: Map<string, readonly string[]>,
129
+ ): CompletionSpec {
130
+ const commands: CompletionCommand[] = [];
131
+ let root: CompletionSpec["root"] = { flags: [], args: [] };
132
+ for (const [name, Cmd] of config.commands) {
133
+ const flags = buildFlags(Cmd);
134
+ const args = buildArgs(Cmd);
135
+ if (name === rootName) {
136
+ root = { flags, args };
137
+ continue;
138
+ }
139
+ if (Cmd.hidden) continue;
140
+ commands.push({
141
+ name,
142
+ aliases: aliasMap.get(name) ?? [],
143
+ description: Cmd.description ?? "",
144
+ flags,
145
+ args,
146
+ });
147
+ }
148
+ commands.sort((a, b) => a.name.localeCompare(b.name));
149
+ return { bin: config.bin, root, commands };
150
+ }
151
+
152
+ // --- Shared helpers -----------------------------------------------------------
153
+
154
+ /** Every value source except a bare boolean flag consumes the following token. */
155
+ function takesValue(v: ValueSource): boolean {
156
+ return v.kind !== "flag";
157
+ }
158
+
159
+ /** All token forms (`name` + aliases) under which a subcommand can be invoked. */
160
+ function commandTokens(c: CompletionCommand): string[] {
161
+ return [c.name, ...c.aliases];
162
+ }
163
+
164
+ export function generateCompletion(shell: Shell, spec: CompletionSpec): string {
165
+ switch (shell) {
166
+ case "bash":
167
+ return generateBash(spec);
168
+ case "zsh":
169
+ return generateZsh(spec);
170
+ case "fish":
171
+ return generateFish(spec);
172
+ }
173
+ }
174
+
175
+ // --- bash ---------------------------------------------------------------------
176
+
177
+ /** Escape for use inside a bash double-quoted `compgen -W "…"` word list. */
178
+ function bashWords(values: readonly string[]): string {
179
+ return values.join(" ").replace(/"/g, '\\"');
180
+ }
181
+
182
+ /** bash snippet that fills COMPREPLY for a flag value, then `return 0`. */
183
+ function bashValueBranch(bin: string, v: ValueSource): string {
184
+ switch (v.kind) {
185
+ case "flag":
186
+ case "value":
187
+ return "return 0";
188
+ case "enum":
189
+ return `COMPREPLY=( $(compgen -W "${bashWords(v.values)}" -- "$cur") ); return 0`;
190
+ case "list":
191
+ return `_omp_comma "${bashWords(v.values)}"; return 0`;
192
+ case "models":
193
+ return v.multiple
194
+ ? `_omp_comma "$(command ${bin} __complete models 2>/dev/null | cut -f1)"; return 0`
195
+ : `COMPREPLY=( $(compgen -W "$(command ${bin} __complete models -- "$cur" 2>/dev/null | cut -f1)" -- "$cur") ); return 0`;
196
+ case "sessions":
197
+ return `COMPREPLY=( $(compgen -W "$(command ${bin} __complete sessions -- "$cur" 2>/dev/null | cut -f1)" -- "$cur") ); return 0`;
198
+ case "file":
199
+ return `COMPREPLY=( $(compgen -f -- "$cur") ); compopt -o filenames; return 0`;
200
+ case "dir":
201
+ return `COMPREPLY=( $(compgen -d -- "$cur") ); compopt -o filenames; return 0`;
202
+ }
203
+ }
204
+
205
+ /** Build the `case "$prev" in …` arms for every value-taking flag in scope. */
206
+ function bashFlagCase(bin: string, flags: CompletionFlag[]): string {
207
+ const lines: string[] = [];
208
+ for (const f of flags) {
209
+ if (!takesValue(f.value)) continue;
210
+ const labels = [`--${f.name}`, ...(f.char ? [`-${f.char}`] : [])];
211
+ lines.push(`\t\t${labels.join("|")})\n\t\t\t${bashValueBranch(bin, f.value)}\n\t\t\t;;`);
212
+ }
213
+ return lines.join("\n");
214
+ }
215
+
216
+ function bashFlagWords(flags: CompletionFlag[]): string {
217
+ const words: string[] = [];
218
+ for (const f of flags) {
219
+ words.push(`--${f.name}`);
220
+ if (f.char) words.push(`-${f.char}`);
221
+ }
222
+ return words.join(" ");
223
+ }
224
+
225
+ function generateBash(spec: CompletionSpec): string {
226
+ const { bin } = spec;
227
+ const parts: string[] = [];
228
+ parts.push(`# bash completion for ${bin} — generated by \`${bin} completions bash\``);
229
+ parts.push("");
230
+
231
+ // Comma-aware static/dynamic list completion helper.
232
+ parts.push(`_omp_comma() {
233
+ local words="$1" realcur prefix
234
+ realcur="\${cur##*,}"
235
+ prefix="\${cur%"$realcur"}"
236
+ local -a matches
237
+ matches=( $(compgen -W "$words" -- "$realcur") )
238
+ local i
239
+ for (( i=0; i < \${#matches[@]}; i++ )); do matches[i]="$prefix\${matches[i]}"; done
240
+ COMPREPLY=( "\${matches[@]}" )
241
+ compopt -o nospace 2>/dev/null
242
+ }`);
243
+ parts.push("");
244
+
245
+ // Root handler: top-level flags + subcommand names.
246
+ const subTokens = spec.commands.flatMap(commandTokens).sort();
247
+ parts.push(`_omp_root() {
248
+ case "$prev" in
249
+ ${bashFlagCase(bin, spec.root.flags)}
250
+ esac
251
+ if [[ "$cur" == -* ]]; then
252
+ COMPREPLY=( $(compgen -W "${bashFlagWords(spec.root.flags)}" -- "$cur") )
253
+ else
254
+ COMPREPLY=( $(compgen -W "${bashWords(subTokens)} ${bashFlagWords(spec.root.flags)}" -- "$cur") )
255
+ fi
256
+ }`);
257
+ parts.push("");
258
+
259
+ // Per-subcommand handlers.
260
+ for (const c of spec.commands) {
261
+ const argEnum = c.args.find(a => a.value.kind === "enum");
262
+ const argWords = argEnum && argEnum.value.kind === "enum" ? bashWords(argEnum.value.values) : "";
263
+ const fileArg = c.args.some(a => a.value.kind === "file");
264
+ const elseBranch = argWords
265
+ ? `COMPREPLY=( $(compgen -W "${argWords}" -- "$cur") )`
266
+ : fileArg
267
+ ? `COMPREPLY=( $(compgen -f -- "$cur") ); compopt -o filenames`
268
+ : ":";
269
+ parts.push(`_omp_cmd_${bashFn(c.name)}() {
270
+ case "$prev" in
271
+ ${bashFlagCase(bin, c.flags)}
272
+ esac
273
+ if [[ "$cur" == -* ]]; then
274
+ COMPREPLY=( $(compgen -W "${bashFlagWords(c.flags)}" -- "$cur") )
275
+ else
276
+ ${elseBranch}
277
+ fi
278
+ }`);
279
+ parts.push("");
280
+ }
281
+
282
+ // Dispatcher.
283
+ const dispatch: string[] = [];
284
+ for (const c of spec.commands) {
285
+ dispatch.push(`\t\t${commandTokens(c).join("|")})\n\t\t\t_omp_cmd_${bashFn(c.name)}\n\t\t\t;;`);
286
+ }
287
+ parts.push(`_omp() {
288
+ local cur prev cmd i
289
+ cur="\${COMP_WORDS[COMP_CWORD]}"
290
+ prev="\${COMP_WORDS[COMP_CWORD-1]}"
291
+ cmd=""
292
+ for (( i=1; i < COMP_CWORD; i++ )); do
293
+ case "\${COMP_WORDS[i]}" in
294
+ -*) ;;
295
+ *) cmd="\${COMP_WORDS[i]}"; break ;;
296
+ esac
297
+ done
298
+ case "$cmd" in
299
+ ${dispatch.join("\n")}
300
+ *) _omp_root ;;
301
+ esac
302
+ }
303
+ complete -F _omp ${bin}`);
304
+ parts.push("");
305
+ return `${parts.join("\n")}\n`;
306
+ }
307
+
308
+ function bashFn(name: string): string {
309
+ return name.replace(/[^A-Za-z0-9]/g, "_");
310
+ }
311
+
312
+ // --- zsh ----------------------------------------------------------------------
313
+
314
+ /** Sanitize a description for embedding in a single-quoted zsh `_arguments` spec. */
315
+ function zshDesc(s: string): string {
316
+ return s
317
+ .replace(/'/g, "’")
318
+ .replace(/\[/g, "(")
319
+ .replace(/\]/g, ")")
320
+ .replace(/[\r\n]+/g, " ")
321
+ .replace(/:/g, " ")
322
+ .trim();
323
+ }
324
+
325
+ function zshAction(v: ValueSource): string {
326
+ switch (v.kind) {
327
+ case "flag":
328
+ return "";
329
+ case "value":
330
+ return ":value:";
331
+ case "enum":
332
+ return `:value:(${v.values.join(" ")})`;
333
+ case "list":
334
+ return ":value:_omp_tools";
335
+ case "models":
336
+ return v.multiple ? ":models:_omp_models_list" : ":model:_omp_call models";
337
+ case "sessions":
338
+ return ":session:_omp_call sessions";
339
+ case "file":
340
+ return ":file:_files";
341
+ case "dir":
342
+ return ":dir:_files -/";
343
+ }
344
+ }
345
+
346
+ function zshFlagSpec(f: CompletionFlag): string {
347
+ const body = `[${zshDesc(f.description)}]${zshAction(f.value)}`;
348
+ if (f.char && f.repeatable) return `'*'{-${f.char},--${f.name}}'${body}'`;
349
+ if (f.char) return `'(-${f.char} --${f.name})'{-${f.char},--${f.name}}'${body}'`;
350
+ if (f.repeatable) return `'*--${f.name}${body}'`;
351
+ return `'--${f.name}${body}'`;
352
+ }
353
+
354
+ function zshArgSpec(f: CompletionArg): string {
355
+ switch (f.value.kind) {
356
+ case "enum":
357
+ return `':${f.name}:(${f.value.values.join(" ")})'`;
358
+ default:
359
+ return `':${f.name}:_files'`;
360
+ }
361
+ }
362
+
363
+ function generateZsh(spec: CompletionSpec): string {
364
+ const { bin } = spec;
365
+ // The `:value:_omp_tools` action references this helper; bake its candidates
366
+ // from the spec's `list` flag so the generator stays a pure function of its
367
+ // input (bash/fish read `v.values` inline for the same reason).
368
+ const listFlag = [...spec.root.flags, ...spec.commands.flatMap(c => c.flags)].find(f => f.value.kind === "list");
369
+ const toolNames = listFlag?.value.kind === "list" ? listFlag.value.values.join(" ") : "";
370
+ const parts: string[] = [];
371
+ parts.push(`#compdef ${bin}`);
372
+ parts.push(`# zsh completion for ${bin} — generated by \`${bin} completions zsh\``);
373
+ parts.push("");
374
+
375
+ // Dynamic helpers (single source: `<bin> __complete <kind>` → value<TAB>desc).
376
+ parts.push(`_omp_call() {
377
+ local kind=$1
378
+ local -a items
379
+ local line
380
+ for line in "\${(@f)$(command ${bin} __complete $kind -- "$PREFIX" 2>/dev/null)}"; do
381
+ [[ -z $line ]] && continue
382
+ items+=( "\${line//$'\\t'/:}" )
383
+ done
384
+ _describe -t "$kind" "$kind" items
385
+ }
386
+ _omp_models_list() {
387
+ local -a items
388
+ local line
389
+ for line in "\${(@f)$(command ${bin} __complete models 2>/dev/null)}"; do
390
+ [[ -z $line ]] && continue
391
+ items+=( "\${line%%$'\\t'*}" )
392
+ done
393
+ _values -s , 'models' $items
394
+ }
395
+ _omp_tools() { _values -s , 'tools' ${toolNames} }`);
396
+ parts.push("");
397
+
398
+ // Subcommand description table.
399
+ const cmdRows = spec.commands.map(c => `\t\t'${c.name}:${zshDesc(c.description)}'`).join("\n");
400
+ parts.push(`_omp_commands() {
401
+ local -a commands
402
+ commands=(
403
+ ${cmdRows}
404
+ )
405
+ _describe -t commands 'command' commands
406
+ }`);
407
+ parts.push("");
408
+
409
+ // Per-subcommand argument functions.
410
+ for (const c of spec.commands) {
411
+ const specs = ["'(-h --help)'{-h,--help}'[Show help]'", ...c.flags.map(zshFlagSpec), ...c.args.map(zshArgSpec)];
412
+ parts.push(`_omp_cmd_${bashFn(c.name)}() {
413
+ _arguments -s \\
414
+ ${specs.join(" \\\n\t\t")}
415
+ }`);
416
+ parts.push("");
417
+ }
418
+
419
+ // Top-level dispatch.
420
+ const aliasArms = spec.commands
421
+ .map(c => `\t\t\t${commandTokens(c).join("|")}) _omp_cmd_${bashFn(c.name)} ;;`)
422
+ .join("\n");
423
+ const rootSpecs = [
424
+ "'(-h --help)'{-h,--help}'[Show help]'",
425
+ "'(-v --version)'{-v,--version}'[Show version]'",
426
+ ...spec.root.flags.map(zshFlagSpec),
427
+ "'1: :_omp_commands'",
428
+ "'*::arg:->args'",
429
+ ];
430
+ parts.push(`_omp() {
431
+ local curcontext="$curcontext" state line
432
+ typeset -A opt_args
433
+ _arguments -C -s \\
434
+ ${rootSpecs.join(" \\\n\t\t")}
435
+ case $state in
436
+ args)
437
+ case $line[1] in
438
+ ${aliasArms}
439
+ esac
440
+ ;;
441
+ esac
442
+ }
443
+ # Works both ways: autoloaded from $fpath (file named _omp) or eval'd from a
444
+ # startup file. When autoloaded, funcstack[1] is _omp and we invoke it; when
445
+ # sourced/eval'd we register it with compdef instead.
446
+ if [ "$funcstack[1]" = "_omp" ]; then
447
+ _omp "$@"
448
+ else
449
+ compdef _omp ${bin}
450
+ fi`);
451
+ parts.push("");
452
+ return `${parts.join("\n")}\n`;
453
+ }
454
+
455
+ // --- fish ---------------------------------------------------------------------
456
+
457
+ function fishDesc(s: string): string {
458
+ return s
459
+ .replace(/'/g, "’")
460
+ .replace(/[\r\n]+/g, " ")
461
+ .trim();
462
+ }
463
+
464
+ function fishValue(bin: string, v: ValueSource): string {
465
+ switch (v.kind) {
466
+ case "flag":
467
+ return "";
468
+ case "value":
469
+ return "-x";
470
+ case "enum":
471
+ case "list":
472
+ return `-x -a '${v.values.join(" ")}'`;
473
+ case "models":
474
+ return `-x -a '(command ${bin} __complete models -- (commandline -ct))'`;
475
+ case "sessions":
476
+ return `-x -a '(command ${bin} __complete sessions -- (commandline -ct))'`;
477
+ case "file":
478
+ return "-r -F";
479
+ case "dir":
480
+ return "-x -a '(__fish_complete_directories (commandline -ct))'";
481
+ }
482
+ }
483
+
484
+ function fishFlagLine(bin: string, cond: string, f: CompletionFlag): string {
485
+ const segs = [`complete -c ${bin}`, `-n '${cond}'`];
486
+ if (f.char) segs.push(`-s ${f.char}`);
487
+ segs.push(`-l ${f.name}`);
488
+ if (f.description) segs.push(`-d '${fishDesc(f.description)}'`);
489
+ const val = fishValue(bin, f.value);
490
+ if (val) segs.push(val);
491
+ return segs.join(" ");
492
+ }
493
+
494
+ function generateFish(spec: CompletionSpec): string {
495
+ const { bin } = spec;
496
+ const lines: string[] = [];
497
+ lines.push(`# fish completion for ${bin} — generated by \`${bin} completions fish\``);
498
+ lines.push("");
499
+
500
+ const allTokens = spec.commands.flatMap(commandTokens);
501
+ lines.push(`function __fish_omp_no_subcommand`);
502
+ lines.push(`\tfor i in (commandline -opc)`);
503
+ lines.push(`\t\tif contains -- $i ${allTokens.join(" ")}`);
504
+ lines.push(`\t\t\treturn 1`);
505
+ lines.push(`\t\tend`);
506
+ lines.push(`\tend`);
507
+ lines.push(`\treturn 0`);
508
+ lines.push(`end`);
509
+ lines.push("");
510
+
511
+ const rootCond = "__fish_omp_no_subcommand";
512
+
513
+ // Subcommand names.
514
+ for (const c of spec.commands) {
515
+ for (const token of commandTokens(c)) {
516
+ lines.push(`complete -c ${bin} -f -n '${rootCond}' -a '${token}' -d '${fishDesc(c.description)}'`);
517
+ }
518
+ }
519
+ lines.push("");
520
+
521
+ // Top-level flags.
522
+ for (const f of spec.root.flags) {
523
+ lines.push(fishFlagLine(bin, rootCond, f));
524
+ }
525
+ lines.push("");
526
+
527
+ // Per-subcommand flags and positional args.
528
+ for (const c of spec.commands) {
529
+ const cond = `__fish_seen_subcommand_from ${commandTokens(c).join(" ")}`;
530
+ for (const f of c.flags) {
531
+ lines.push(fishFlagLine(bin, cond, f));
532
+ }
533
+ // Positionals: fish conditions can't gate on position, so emit enum
534
+ // candidates (if any) and otherwise a single file completion — never both,
535
+ // and never duplicated across multiple file-typed positionals.
536
+ const enumArgs = c.args.filter(a => a.value.kind === "enum");
537
+ if (enumArgs.length > 0) {
538
+ for (const a of enumArgs) {
539
+ if (a.value.kind !== "enum") continue;
540
+ lines.push(
541
+ `complete -c ${bin} -f -n '${cond}' -a '${a.value.values.join(" ")}' -d '${fishDesc(a.description)}'`,
542
+ );
543
+ }
544
+ } else if (c.args.some(a => a.value.kind === "file")) {
545
+ lines.push(`complete -c ${bin} -F -n '${cond}'`);
546
+ }
547
+ }
548
+ lines.push("");
549
+ return `${lines.join("\n")}\n`;
550
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Setup CLI command handler.
3
3
  *
4
- * Handles `omp setup <component>` to install dependencies for optional features.
4
+ * Handles `omp setup` for onboarding and `omp setup <component>` for optional dependencies.
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { $which, APP_NAME, getPythonEnvDir } from "@oh-my-pi/pi-utils";
@@ -207,9 +207,10 @@ async function handleSttSetup(flags: { json?: boolean; check?: boolean }): Promi
207
207
  * Print setup command help.
208
208
  */
209
209
  export function printSetupHelp(): void {
210
- console.log(`${chalk.bold(`${APP_NAME} setup`)} - Install dependencies for optional features
210
+ console.log(`${chalk.bold(`${APP_NAME} setup`)} - Run onboarding or install dependencies for optional features
211
211
 
212
212
  ${chalk.bold("Usage:")}
213
+ ${APP_NAME} setup Run the onboarding wizard
213
214
  ${APP_NAME} setup <component> [options]
214
215
 
215
216
  ${chalk.bold("Components:")}
@@ -221,7 +222,8 @@ ${chalk.bold("Options:")}
221
222
  --json Output status as JSON
222
223
 
223
224
  ${chalk.bold("Examples:")}
224
- ${APP_NAME} setup python Install Python execution dependencies
225
+ ${APP_NAME} setup Run the onboarding wizard
226
+ ${APP_NAME} setup python Check Python execution dependencies
225
227
  ${APP_NAME} setup stt Install speech-to-text dependencies
226
228
  ${APP_NAME} setup stt --check Check if STT dependencies are available
227
229
  ${APP_NAME} setup python --check Check if Python execution is available
@@ -17,6 +17,8 @@ export const commands: CommandEntry[] = [
17
17
  { name: "auth-gateway", load: () => import("./commands/auth-gateway").then(m => m.default) },
18
18
  { name: "agents", load: () => import("./commands/agents").then(m => m.default) },
19
19
  { name: "commit", load: () => import("./commands/commit").then(m => m.default) },
20
+ { name: "completions", load: () => import("./commands/completions").then(m => m.default) },
21
+ { name: "__complete", load: () => import("./commands/complete").then(m => m.default) },
20
22
  { name: "config", load: () => import("./commands/config").then(m => m.default) },
21
23
  { name: "grep", load: () => import("./commands/grep").then(m => m.default) },
22
24
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
package/src/cli.ts CHANGED
@@ -1,16 +1,10 @@
1
1
  #!/usr/bin/env bun
2
- import { APP_NAME, MIN_BUN_VERSION, procmgr, VERSION } from "@oh-my-pi/pi-utils";
3
-
4
- // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
5
- // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
6
- // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
7
- procmgr.scrubProcessEnv();
8
-
9
2
  /**
10
3
  * CLI entry point — registers all commands explicitly and delegates to the
11
4
  * lightweight CLI runner from pi-utils.
12
5
  */
13
6
  import { type CliConfig, run } from "@oh-my-pi/pi-utils/cli";
7
+ import { APP_NAME, MIN_BUN_VERSION, VERSION } from "@oh-my-pi/pi-utils/dirs";
14
8
  import { commands, isSubcommand } from "./cli-commands";
15
9
 
16
10
  if (Bun.semver.order(Bun.version, MIN_BUN_VERSION) < 0) {