@agentwhy/cli 0.2.0 → 0.2.1

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.
@@ -3,12 +3,16 @@ const HELP_FLAGS = ['--help', '-h'];
3
3
  export class CommandRouter {
4
4
  #commands;
5
5
  #withoutACommand;
6
+ #missingCommand;
6
7
  /**
7
8
  * `withoutACommand` answers `agentwhy` with nothing after it, and with arguments that begin with `-` (they are its
8
9
  * flags); without one, that stays a usage error. It may also be registered under its own name.
10
+ *
11
+ * `missingCommand` is said under that usage error alone, for whoever ran a bare `agentwhy` where nothing answers it.
9
12
  */
10
- constructor(commands, withoutACommand) {
13
+ constructor(commands, withoutACommand, missingCommand) {
11
14
  this.#withoutACommand = withoutACommand;
15
+ this.#missingCommand = missingCommand;
12
16
  const byName = new Map();
13
17
  for (const command of commands) {
14
18
  if (byName.has(command.name))
@@ -27,7 +31,9 @@ export class CommandRouter {
27
31
  if (name !== undefined && HELP_FLAGS.includes(name))
28
32
  return { kind: 'help', usage: this.usage };
29
33
  if (name === undefined) {
30
- return this.#withoutACommand === undefined ? this.#usageError('missing command') : this.#withoutACommand.execute([]);
34
+ if (this.#withoutACommand !== undefined)
35
+ return this.#withoutACommand.execute([]);
36
+ return { ...this.#usageError('missing command'), ...(this.#missingCommand === undefined ? {} : { hint: this.#missingCommand }) };
31
37
  }
32
38
  if (name.startsWith('-') && this.#withoutACommand !== undefined)
33
39
  return this.#withoutACommand.execute(argv);
package/dist/cli.js CHANGED
@@ -71,7 +71,8 @@ function write(result) {
71
71
  process.stdout.write(result.usage);
72
72
  return EXIT_CODE.ok;
73
73
  case 'usage-error':
74
- process.stderr.write(`agentwhy: ${result.message}\n\n${result.usage}`);
74
+ // R73: the message is the first thing on stderr, before any help this one carries.
75
+ process.stderr.write(`agentwhy: ${result.message}\n\n${result.hint === undefined ? '' : `${result.hint}\n\n`}${result.usage}`);
75
76
  return EXIT_CODE.usage;
76
77
  case 'completed':
77
78
  process.stdout.write(result.output);
@@ -71,6 +71,8 @@ import { ConversationsRenderer } from './report/start/conversations/conversation
71
71
  import { SettingsRenderer } from './report/start/settings/settings-renderer.js';
72
72
  import { MonthRenderer } from './report/start/month/month-renderer.js';
73
73
  import { ToFixRenderer } from './report/start/to-fix/to-fix-renderer.js';
74
+ import { projectName } from './report/start/app-nav.js';
75
+ import { welcomeIn } from './report/start/onboarding/welcome-decision.js';
74
76
  import { SessionStart } from './report/start/session-start.js';
75
77
  import { NoticeWordsRenderer } from './report/watch/render/notice-words.js';
76
78
  import { NoticeSettings } from './report/watch/notice-settings.js';
@@ -393,6 +395,19 @@ export function createCommandRouter(environment) {
393
395
  files: start,
394
396
  sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
395
397
  clock: () => Date.now(),
398
+ // AW1: the address `--detach` hands over names the welcome where this project is to meet it, as the address a
399
+ // served run prints does. The same decision the run itself asks (W23), from the same record and settings files.
400
+ welcome: {
401
+ file: APP_LINKS.onboarding,
402
+ project: projectName(environment.workingDirectory),
403
+ opens: async () => (await welcomeIn(environment.workingDirectory, {
404
+ store: onboarding,
405
+ files,
406
+ home: environment.home,
407
+ realHome,
408
+ now: () => Date.now(),
409
+ })).opens,
410
+ },
396
411
  });
397
412
  const start_ = new StartCliCommand({ start, detached, now: environment.now });
398
413
  const check_ = new CheckCliCommand({ check, now: environment.now });
@@ -472,5 +487,10 @@ export function createCommandRouter(environment) {
472
487
  interactive: environment.inputIsTerminal,
473
488
  }),
474
489
  new CodexStopCliCommand(codexWatch, environment.inputIsTerminal),
475
- ], environment.interactive ? start_ : undefined);
490
+ // R73 stands (`a-page-not-a-file` PFD3): off a terminal a bare `agentwhy` opens nothing and serves nothing. But the
491
+ // reader of that refusal is usually an AI agent asked to start agentwhy and send a link, and the usage alone left it
492
+ // guessing: measured 2026-10-04 in three apps, one guessed `start --detach` and one gave up. So the refusal names it.
493
+ ], environment.interactive ? start_ : undefined, 'There is no terminal here, so a bare agentwhy opens nothing and serves nothing.\n' +
494
+ 'To serve a page and return at once - what an agent asked to send someone a link needs - run:\n' +
495
+ ' agentwhy start --detach');
476
496
  }
@@ -1,3 +1,4 @@
1
+ import { printable } from '../../shared/printable.js';
1
2
  import { quietlySaid } from './render/start-words.js';
2
3
  /**
3
4
  * How long a server started in the background is waited for, at most, while its process runs (PF3): its first pages are
@@ -34,12 +35,16 @@ export class DetachedStart {
34
35
  }
35
36
  return this.#asFiles(options);
36
37
  }
37
- /** The page asked for on a running server: the session's report where it serves one, else every conversation (SW10). */
38
+ /**
39
+ * The page asked for on a running server: the session's report where it serves one (SW10), else the welcome where
40
+ * this project is to meet it (AW1), else every conversation.
41
+ */
38
42
  async #show(base, options, reused) {
39
43
  const { probe, browser } = this.#dependencies;
40
44
  const asked = options.session === undefined ? undefined : `${base}${options.session}.html`;
41
45
  const found = asked === undefined || (await probe.answers(asked));
42
- const url = asked !== undefined && found ? asked : `${base}index.html`;
46
+ const welcome = await this.#welcome(asked);
47
+ const url = asked !== undefined && found ? asked : `${base}${welcome?.file ?? 'index.html'}`;
43
48
  const opened = options.open && (await browser.open(url));
44
49
  // A conversation asked for and not served is said in every output, never passed off as its report (SW10).
45
50
  const missing = found ? '' : 'That conversation was not found here, so all conversations are served.\n';
@@ -50,9 +55,23 @@ export class DetachedStart {
50
55
  outcome: 'written',
51
56
  output: missing +
52
57
  `${reused ? 'agentwhy was already running for this project' : 'agentwhy runs in the background for this project'}: ${url}\n` +
58
+ // AWD3's sentence, in the words a served run already prints: true whether or not anyone opens the address.
59
+ (welcome === undefined
60
+ ? ''
61
+ : `${opened ? 'Opened' : 'That address opens'} the welcome page, to set agentwhy up for ${printable(welcome.project)}.\n`) +
53
62
  'It stops by itself 30 minutes after its last page is closed.\n',
54
63
  };
55
64
  }
65
+ /**
66
+ * AW1: the onboarding's file where this run's address is to name it. A conversation asked for is the person's own
67
+ * yes to that report and wins over a setup they did not ask for, as a served run's own page does (SW10).
68
+ */
69
+ async #welcome(asked) {
70
+ const { welcome } = this.#dependencies;
71
+ if (asked !== undefined || welcome === undefined || !(await welcome.opens()))
72
+ return undefined;
73
+ return { file: welcome.file, project: welcome.project };
74
+ }
56
75
  /** PF4: no server came up - a sandbox, a refusal - so the pages are files, and what would serve them is said. */
57
76
  async #asFiles(options) {
58
77
  const written = await this.#dependencies.files.run({ ...options, serve: false });
@@ -0,0 +1,44 @@
1
+ // Copyright 2026 Nessprim Karol Kozer
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { join } from 'node:path';
4
+ import { SETTINGS_FILES } from '../../../adapter/claude-code/contract/settings.js';
5
+ import { notAProject } from '../../../setup/not-a-project.js';
6
+ import { readSettingsFile, setUpBy } from '../settings-files.js';
7
+ /**
8
+ * Whether a project's run should name the onboarding instead of the index (W23), and what else the page needs to know.
9
+ *
10
+ * Its one side effect is N6's: a project set up before this screen existed, or by hand, has its `done` line written
11
+ * here and silently, so that turning alerts off later never brings the onboarding back. Asking a second time for the
12
+ * same project writes no second line: that reading finds the first.
13
+ *
14
+ * Shared by every run that names a page - `start` itself, and `start --detach`, whose address is the only way a person
15
+ * reaches one (`2026-10-01-the-address-opens-the-welcome.md` AW1, extended to `--detach` on 2026-10-04).
16
+ */
17
+ export async function welcomeIn(workingDirectory, asked) {
18
+ const { store, files, home, realHome, now } = asked;
19
+ const reading = await store.read();
20
+ // W24: the intro plays once per person - where no project of theirs has finished the onboarding.
21
+ const intro = !reading.anywhere && !reading.failed;
22
+ // which-project V7: in the home directory no project is being shown, so the onboarding opens at its project step,
23
+ // whatever the record says - after the welcome only for a person who has never finished one. Nothing here is set up
24
+ // and no record is kept of it: not even the silent `done` line below.
25
+ if (home !== undefined && notAProject(workingDirectory, home, realHome) !== undefined) {
26
+ return { opens: true, intro, setUp: false, ...(intro ? {} : { atProject: true }) };
27
+ }
28
+ if (reading.here)
29
+ return { opens: false, intro, setUp: true };
30
+ if (reading.failed)
31
+ return { opens: false, intro, setUp: false };
32
+ // A local file that cannot be read leaves what runs unknown, and the onboarding closed (as `#settingsNow` does).
33
+ const local = await readSettingsFile(files, join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.local));
34
+ if (local === 'unreadable')
35
+ return { opens: false, intro, setUp: false };
36
+ const shared = await readSettingsFile(files, join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.shared));
37
+ // N6, widened 2026-09-28 by the maintainer (which-project V10): a project where one of agentwhy's hooks runs, or whose
38
+ // settings block files, was set up - before this screen existed, or by hand - and is recorded so, silently.
39
+ if (setUpBy([local, shared])) {
40
+ await store.add(now());
41
+ return { opens: false, intro, setUp: true };
42
+ }
43
+ return { opens: true, intro, setUp: false };
44
+ }
@@ -75,7 +75,12 @@ function page(said, view) {
75
75
  ...(said.welcome === 'opened' ? [`Opened the welcome page, to set agentwhy up for ${said.project ?? 'this project'}.`] : []),
76
76
  ...(said.welcome === 'served' ? [`That address opens the welcome page, to set agentwhy up for ${said.project ?? 'this project'}.`] : []),
77
77
  ...(said.servedAsFile === true
78
- ? [warn('The page could not be served, so it was opened as a file: marks made on it are copied as commands.', view)]
78
+ ? [
79
+ warn('The page could not be served, so it was opened as a file: marks made on it are copied as commands.', view),
80
+ // PF4's way out, which `--detach` has always said and this run did not: a sandbox is the usual reason, and a
81
+ // person reading a file cannot finish a setup or make a mark on it.
82
+ dim('To open it live, run this command again outside the sandbox, or type npx @agentwhy/cli in a terminal.', view),
83
+ ]
79
84
  : []),
80
85
  dim(said.shared
81
86
  ? 'Shared view: session ids and the project path are not shown, and nothing above the project root appears in any report.'
@@ -19,7 +19,7 @@ import { indexHandler } from './serve/index-handler.js';
19
19
  import { movedPage } from './projects/moved-page.js';
20
20
  import { nearestProject } from '../../core/nearest-project.js';
21
21
  import { translator } from '../render/report-copy.js';
22
- import { hookComplete, readSettingsFile, setUpBy } from './settings-files.js';
22
+ import { hookComplete, readSettingsFile } from './settings-files.js';
23
23
  import { indexProjects } from './projects/index-projects.js';
24
24
  import { NOT_A_PROJECT } from '../../setup/project-setup.js';
25
25
  import { notAProject } from '../../setup/not-a-project.js';
@@ -31,6 +31,7 @@ import { DEFAULT_THRESHOLD } from '../watch/agent-alert.js';
31
31
  import { DEFAULT_CHANNELS, DEFAULT_CLEAN, DEFAULT_SAID_AS } from '../watch/notice-choices.js';
32
32
  import { actionsAfterMarks, markLines, marksInRange, standingMarks } from '../check/marks.js';
33
33
  import { finishOnboarding } from './onboarding/finish-onboarding.js';
34
+ import { welcomeIn } from './onboarding/welcome-decision.js';
34
35
  import { WHO_STEP } from './onboarding/onboarding-script.js';
35
36
  import { repositoryAbove } from './repository.js';
36
37
  import { pageStopped, quietlySaid, startSaid } from './render/start-words.js';
@@ -684,33 +685,17 @@ export class SessionStart {
684
685
  * alerts off later never brings the onboarding back.
685
686
  */
686
687
  async #welcome(workingDirectory) {
687
- const { onboarding, setup, notices } = this.#dependencies;
688
+ const { onboarding, setup, notices, policyFiles, home, realHome } = this.#dependencies;
689
+ // The onboarding writes through both (W15), so a run without them names no page it could not finish.
688
690
  if (onboarding === undefined || setup === undefined || notices === undefined)
689
691
  return undefined;
690
- const reading = await onboarding.store.read();
691
- // W24: the intro plays once per person - where no project of theirs has finished the onboarding.
692
- const intro = !reading.anywhere && !reading.failed;
693
- // which-project V7: in the home directory no project is being shown, so the onboarding opens at its project step,
694
- // whatever the record says - after the welcome only for a person who has never finished one. Nothing here is set up
695
- // and no record is kept of it: not even the silent `done` line below.
696
- if (this.#notAProject(workingDirectory) !== undefined)
697
- return { opens: true, intro, setUp: false, ...(intro ? {} : { atProject: true }) };
698
- if (reading.here)
699
- return { opens: false, intro, setUp: true };
700
- if (reading.failed)
701
- return { opens: false, intro, setUp: false };
702
- // A local file that cannot be read leaves what runs unknown, and the onboarding closed (as `#settingsNow` does).
703
- const local = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.local));
704
- if (local === 'unreadable')
705
- return { opens: false, intro, setUp: false };
706
- const shared = await this.#settingsFile(join(workingDirectory, SETTINGS_FILES.directory, SETTINGS_FILES.shared));
707
- // N6, widened 2026-09-28 by the maintainer (which-project V10): a project where one of agentwhy's hooks runs, or whose
708
- // settings block files, was set up - before this screen existed, or by hand - and is recorded so, silently.
709
- if (setUpBy([local, shared])) {
710
- await onboarding.store.add((this.#dependencies.clock ?? (() => this.#dependencies.now))());
711
- return { opens: false, intro, setUp: true };
712
- }
713
- return { opens: true, intro, setUp: false };
692
+ return welcomeIn(workingDirectory, {
693
+ store: onboarding.store,
694
+ files: policyFiles,
695
+ ...(home === undefined ? {} : { home }),
696
+ ...(realHome === undefined ? {} : { realHome }),
697
+ now: this.#dependencies.clock ?? (() => this.#dependencies.now),
698
+ });
714
699
  }
715
700
  /**
716
701
  * The person's projects, read again with every refresh as the settings are, since a project set up in another tab is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentwhy/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Reconstructs from transcripts why an AI agent reached for protected data",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",