@namzu/cli 2.2.0 → 2.5.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.
@@ -0,0 +1,208 @@
1
+ /**
2
+ * User-defined slash commands — one `.md` file per command.
3
+ *
4
+ * A command is a markdown file whose body is a prompt template:
5
+ *
6
+ * ~/.namzu/commands/<name>.md available everywhere
7
+ * <cwd>/.namzu/commands/<name>.md this project only
8
+ *
9
+ * The name is the filename without `.md`, so `review.md` is `/review`. A
10
+ * project command shadows a user command of the same name, which is the same
11
+ * precedence skills use.
12
+ *
13
+ * ## Why this is not `discoverSkills`
14
+ *
15
+ * The kernel's `discoverSkills` finds DIRECTORIES containing a `SKILL.md`, and
16
+ * returns `[]` for a folder of loose `.md` files — silently, because an empty
17
+ * roster is a legitimate answer to "no skills here". A command is one file, so
18
+ * that loader cannot serve this layout and would report nothing rather than
19
+ * fail. What IS reused is the part worth sharing: `parseFrontmatter`, so there
20
+ * is exactly one definition of what a `---` block means anywhere in the repo.
21
+ *
22
+ * ## Arguments
23
+ *
24
+ * `$ARGUMENTS` in the template is replaced by whatever followed the command.
25
+ * A template WITHOUT it, invoked WITH arguments, is refused rather than run —
26
+ * see `expandCommand`. Dropping them silently is the failure this file is
27
+ * written to avoid, and appending them somewhere the author did not ask for
28
+ * would be guessing where they belong.
29
+ */
30
+ import { readFileSync, readdirSync } from 'node:fs';
31
+ import { homedir } from 'node:os';
32
+ import { join } from 'node:path';
33
+ import { parseFrontmatter } from '@namzu/sdk';
34
+ /** The token a template uses to receive what followed the command. */
35
+ export const ARGUMENTS_TOKEN = '$ARGUMENTS';
36
+ export function userCommandsDir(home = homedir()) {
37
+ return join(home, '.namzu', 'commands');
38
+ }
39
+ export function projectCommandsDir(cwd = process.cwd()) {
40
+ return join(cwd, '.namzu', 'commands');
41
+ }
42
+ function readCommandsFrom(dir, source) {
43
+ let files;
44
+ try {
45
+ files = readdirSync(dir, { withFileTypes: true })
46
+ .filter((e) => e.isFile() && e.name.endsWith('.md'))
47
+ .map((e) => e.name);
48
+ }
49
+ catch {
50
+ // No directory is not an error: most projects define no commands.
51
+ return [];
52
+ }
53
+ const out = [];
54
+ for (const file of files) {
55
+ const path = join(dir, file);
56
+ const name = file.slice(0, -'.md'.length);
57
+ let raw;
58
+ try {
59
+ raw = readFileSync(path, 'utf8');
60
+ }
61
+ catch {
62
+ continue;
63
+ }
64
+ // Same fence-decides rule as the skill reader: a file with no `---` is
65
+ // all body, which is the common case for a command, and a file that
66
+ // opens one must parse. The absence test mirrors the kernel reader's own
67
+ // so the two cannot disagree about what counts as having frontmatter.
68
+ try {
69
+ if (!raw.trimStart().startsWith('---')) {
70
+ out.push({ name, description: '(no description)', template: raw.trim(), path, source });
71
+ continue;
72
+ }
73
+ const { values, body } = parseFrontmatter(raw, path);
74
+ const description = values.description?.kind === 'scalar' ? values.description.value : '(no description)';
75
+ out.push({ name, description, template: body, path, source });
76
+ }
77
+ catch (err) {
78
+ out.push({
79
+ name,
80
+ description: '(could not be read)',
81
+ template: '',
82
+ path,
83
+ source,
84
+ problem: err instanceof Error ? err.message : String(err),
85
+ });
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ /**
91
+ * Every user-defined command, project shadowing user on a name clash.
92
+ *
93
+ * `reserved` names are dropped with a `problem` rather than allowed to win:
94
+ * a file called `help.md` must not replace `/help`, because a builtin someone
95
+ * relies on disappearing when a file appears is the worst kind of surprise —
96
+ * and silently ignoring the file would leave its author with no idea why it
97
+ * never ran.
98
+ */
99
+ export function discoverUserCommands(opts = {}) {
100
+ const reserved = new Set(opts.reserved ?? []);
101
+ const user = readCommandsFrom(userCommandsDir(opts.home), 'user');
102
+ const project = readCommandsFrom(projectCommandsDir(opts.cwd), 'project');
103
+ const byName = new Map();
104
+ for (const c of user)
105
+ byName.set(c.name, c);
106
+ for (const c of project)
107
+ byName.set(c.name, c); // project wins
108
+ return [...byName.values()]
109
+ .map((c) => reserved.has(c.name)
110
+ ? { ...c, problem: `"/${c.name}" is a built-in command, so this file is not used.` }
111
+ : c)
112
+ .sort((a, b) => a.name.localeCompare(b.name));
113
+ }
114
+ /**
115
+ * Resolve a headless prompt that may name one of the operator's own commands.
116
+ *
117
+ * `namzu run "/ozet hedef.js"` used to send that string to the model verbatim.
118
+ * The model, reasonably, tried to make sense of it — offering to create a file
119
+ * called `ozet hedef.js`. The run exited 0 with confident output that had
120
+ * nothing to do with the command. It did not fail; it quietly did something
121
+ * else, which is the shape worth removing.
122
+ *
123
+ * ## Why a leading `/` is not enough to call it a command
124
+ *
125
+ * `namzu run "/usr/local/bin is missing"` and `namzu run "/clear the cache in
126
+ * redis"` are ordinary prompts. Treating every leading slash as a command would
127
+ * break them, and breaking a working prompt to fix a broken one is not a trade
128
+ * worth making. So the test is not the slash — it is whether the first token
129
+ * names a command **this project actually declares**.
130
+ *
131
+ * That asymmetry is the whole rule. A file in `.namzu/commands/` is an explicit
132
+ * declaration by the operator, so matching it is high-confidence. A built-in's
133
+ * name is a common English word that nobody declared, so matching it is not.
134
+ *
135
+ * The one exception is a prompt that is EXACTLY a built-in and nothing else:
136
+ * `namzu run "/help"` is not a sentence anybody means literally, and answering
137
+ * it with a model improvising on the string is the same silent misfire. With
138
+ * arguments — `/clear the cache` — it stays prose, because there the words
139
+ * carry a meaning the command name does not.
140
+ */
141
+ export function expandHeadlessCommand(prompt, opts = {}) {
142
+ const trimmed = prompt.trim();
143
+ if (!trimmed.startsWith('/'))
144
+ return { kind: 'unchanged', prompt };
145
+ const [token, ...rest] = trimmed.slice(1).split(/\s+/);
146
+ const name = token ?? '';
147
+ if (!name)
148
+ return { kind: 'unchanged', prompt };
149
+ const builtins = new Set(opts.builtins ?? []);
150
+ if (rest.length === 0 && builtins.has(name)) {
151
+ return {
152
+ kind: 'refused',
153
+ reason: `/${name} is an interactive command and does nothing in \`namzu run\`. Run \`namzu\` for the terminal agent, or pass a prompt instead.`,
154
+ };
155
+ }
156
+ const commands = discoverUserCommands({
157
+ ...(opts.home !== undefined ? { home: opts.home } : {}),
158
+ ...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
159
+ reserved: [...builtins],
160
+ });
161
+ const found = commands.find((c) => c.name === name);
162
+ if (!found)
163
+ return { kind: 'unchanged', prompt };
164
+ const expanded = expandCommand(found, rest.join(' '));
165
+ return expanded.ok
166
+ ? { kind: 'expanded', prompt: expanded.prompt, name }
167
+ : { kind: 'refused', reason: expanded.reason };
168
+ }
169
+ /**
170
+ * Fill a command's template with the arguments it was invoked with.
171
+ *
172
+ * Three cases, and the third is the decision worth defending:
173
+ *
174
+ * 1. Template has `$ARGUMENTS` → substituted, with `''` when none were given.
175
+ * 2. No `$ARGUMENTS`, no arguments → the template is a static prompt. Fine.
176
+ * 3. No `$ARGUMENTS`, arguments given → **refused**, naming the file and the
177
+ * fix.
178
+ *
179
+ * The third could have appended them under a heading instead, and that is
180
+ * friendlier in the moment. It was rejected because it guesses where the author
181
+ * wanted them and because the author never finds out their template ignores
182
+ * arguments. Refusing is one-time friction that teaches the contract.
183
+ *
184
+ * It is also the reversible direction: relaxing a refusal into an append later
185
+ * breaks nobody, while tightening an append into a refusal breaks everyone who
186
+ * had come to rely on it. Where a contract is hard to change, pick the one that
187
+ * can still be changed.
188
+ */
189
+ export function expandCommand(command, args) {
190
+ if (command.problem)
191
+ return { ok: false, reason: command.problem };
192
+ const trimmed = args.trim();
193
+ if (command.template.includes(ARGUMENTS_TOKEN)) {
194
+ return { ok: true, prompt: command.template.split(ARGUMENTS_TOKEN).join(trimmed) };
195
+ }
196
+ if (trimmed.length === 0)
197
+ return { ok: true, prompt: command.template };
198
+ return {
199
+ ok: false,
200
+ reason: [
201
+ `/${command.name} takes no arguments, but you gave: ${trimmed}`,
202
+ '',
203
+ `Add ${ARGUMENTS_TOKEN} where they belong in ${command.path}, and they will be`,
204
+ 'substituted there. Running it now would silently discard them.',
205
+ ].join('\n'),
206
+ };
207
+ }
208
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/user-commands/store.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@namzu/cli",
3
- "version": "2.2.0",
3
+ "version": "2.5.0",
4
4
  "description": "Operator CLI for the Namzu agent platform — namzu doctor + future commands. Dual-purpose: standalone bin (`namzu doctor`) and library (`import { runDoctor } from '@namzu/cli'`).",
5
5
  "keywords": [
6
6
  "namzu",
@@ -44,8 +44,8 @@
44
44
  "ink": "^7.0.3",
45
45
  "react": "^19.2.6",
46
46
  "yaml": "^2.9.0",
47
- "@namzu/anthropic": "3.1.1",
48
47
  "@namzu/ollama": "2.0.1",
48
+ "@namzu/anthropic": "3.1.1",
49
49
  "@namzu/openai": "1.1.1",
50
50
  "@namzu/openrouter": "2.0.1",
51
51
  "@namzu/sdk": "^15.1.0"