diff --git a/src/cli.ts b/src/cli.ts index d832bc0..401b49b 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -518,7 +518,16 @@ async function run(argv: string[]): Promise { let command = resolveCommand(positional[0]); let commandArgOffset = 1; - if (invokedAsKnowledge() && command && !COMMANDS.includes(command)) { + // Natural-language shorthand: when invoked as the `knowledge` bin, a multi-word prompt + // is treated as `knowledge ask ` — whether passed as separate words + // (`knowledge how do I cite the handbook`, multiple positionals) or as a single quoted + // string (`knowledge "How do we cite the handbook?"`, one positional containing spaces; + // the canonical documented form). A single bare token with no whitespace is almost always + // a mistyped command (`knowledge lst`, `knowledge boguscmd`), so it is NOT remapped — it + // falls through to the unknown-command handler below and exits non-zero, instead of + // silently running an ask/build search and returning false success to scripts. + const looksLikeNaturalLanguage = positional.length > 1 || /\s/.test(command); + if (invokedAsKnowledge() && command && !COMMANDS.includes(command) && looksLikeNaturalLanguage) { command = 'ask'; commandArgOffset = 0; } diff --git a/tests/cli.test.ts b/tests/cli.test.ts index 82aa934..e732ff2 100644 --- a/tests/cli.test.ts +++ b/tests/cli.test.ts @@ -400,6 +400,51 @@ describe('knowledge cli', () => { expect(err).toContain("Did you mean 'list'"); }); + test('knowledge bin rejects unknown single-token command instead of running ask', () => { + // Regression: when invoked as the `knowledge` bin, an unknown top-level command + // (`knowledge boguscmd`, `knowledge lst`) was silently remapped to an ask/build + // search prompt and exited 0, returning false success to scripts. It must now fail. + for (const bogus of ['boguscmd', 'lst']) { + const result = runKnowledgeBin([bogus]); + expect(result.exitCode).toBe(1); + const out = new TextDecoder().decode(result.stdout); + const err = new TextDecoder().decode(result.stderr); + expect(out).not.toContain('Prepared citation context draft'); + expect(err).toContain(`Unknown command: ${bogus}`); + } + // The `lst` typo should also surface the levenshtein suggestion. + const typo = runKnowledgeBin(['lst']); + expect(new TextDecoder().decode(typo.stderr)).toContain("Did you mean 'list'"); + }, 20000); + + test('knowledge bin keeps multi-word natural-language ask shorthand', () => { + // The documented `knowledge ` shorthand for multi-word prompts must still + // route to ask/build so genuine natural-language queries keep working. Use an + // isolated HOME so the ask searches an empty store (fast, deterministic) rather + // than the operator's real global knowledge DB. + const dir = mkdtempSync(join(tmpdir(), 'ok-nl-ask-')); + const home = mkdtempSync(join(tmpdir(), 'ok-nl-ask-home-')); + const result = runKnowledgeBin(['how', 'do', 'I', 'cite', 'sources', '--scope', 'project', '--json'], dir, isolatedHomeEnv(home)); + expect(result.exitCode).toBe(0); + const out = JSON.parse(new TextDecoder().decode(result.stdout)); + expect(out.ok).toBe(true); + expect(out.prompt).toBe('how do I cite sources'); + }, 20000); + + test('knowledge bin keeps quoted single-token natural-language ask shorthand', () => { + // Regression guard: the canonical documented form passes the whole prompt as one + // quoted argument (`knowledge "How do we cite handbook policy?"`), so it arrives as a + // single positional token that CONTAINS whitespace. It must still route to ask/build + // (not be rejected as an unknown single-token command). + const dir = mkdtempSync(join(tmpdir(), 'ok-nl-ask-quoted-')); + const home = mkdtempSync(join(tmpdir(), 'ok-nl-ask-quoted-home-')); + const result = runKnowledgeBin(['How do we cite handbook policy?', '--scope', 'project', '--json'], dir, isolatedHomeEnv(home)); + expect(result.exitCode).toBe(0); + const out = JSON.parse(new TextDecoder().decode(result.stdout)); + expect(out.ok).toBe(true); + expect(out.prompt).toBe('How do we cite handbook policy?'); + }, 20000); + test('usage/validation errors do not leak an internal stack trace', () => { // Regression: usage/validation errors previously logged the full Error stack // (bundled bin path + minified function names) to stderr. They must show only