@aragon-agent/cli 0.6.25 → 0.6.27

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 (48) hide show
  1. package/CHANGELOG.md +104 -0
  2. package/README.md +194 -13
  3. package/dist/agent/reducer.d.ts +22 -0
  4. package/dist/agent/reducer.js +4 -1
  5. package/dist/cli.js +214 -2
  6. package/dist/commands/branch-command.d.ts +61 -0
  7. package/dist/commands/branch-command.js +117 -0
  8. package/dist/commands/builtins.js +48 -6
  9. package/dist/commands/registry.d.ts +28 -0
  10. package/dist/commands/session-command.d.ts +54 -0
  11. package/dist/commands/session-command.js +109 -0
  12. package/dist/config/load.d.ts +15 -0
  13. package/dist/exec/index.js +10 -1
  14. package/dist/logging/logger.d.ts +1 -1
  15. package/dist/proc/terminal-plan.d.ts +173 -0
  16. package/dist/proc/terminal-plan.js +393 -0
  17. package/dist/session/branch-invocation.d.ts +127 -0
  18. package/dist/session/branch-invocation.js +153 -0
  19. package/dist/session/branch-launch.d.ts +79 -0
  20. package/dist/session/branch-launch.js +265 -0
  21. package/dist/session/checkpoint.d.ts +257 -0
  22. package/dist/session/checkpoint.js +512 -0
  23. package/dist/session/cli-commands.d.ts +2 -0
  24. package/dist/session/cli-commands.js +36 -5
  25. package/dist/session/identity.d.ts +72 -0
  26. package/dist/session/identity.js +148 -0
  27. package/dist/session/limits.d.ts +66 -0
  28. package/dist/session/limits.js +66 -0
  29. package/dist/session/persist.d.ts +30 -5
  30. package/dist/session/startup-session.d.ts +107 -0
  31. package/dist/session/startup-session.js +315 -0
  32. package/dist/session/store.d.ts +60 -1
  33. package/dist/session/store.js +95 -6
  34. package/dist/ui/App.d.ts +54 -2
  35. package/dist/ui/App.js +115 -21
  36. package/dist/ui/Header.js +8 -16
  37. package/dist/ui/StatusBar.d.ts +8 -0
  38. package/dist/ui/StatusBar.js +12 -3
  39. package/dist/ui/layout/header-hints.d.ts +68 -0
  40. package/dist/ui/layout/header-hints.js +138 -0
  41. package/dist/ui/layout/session-chip.d.ts +52 -0
  42. package/dist/ui/layout/session-chip.js +64 -0
  43. package/dist/ui/layout/status-layout.d.ts +10 -0
  44. package/dist/ui/layout/status-layout.js +8 -1
  45. package/dist/ui/overlays/HelpOverlay.js +9 -3
  46. package/dist/ui/session-bridge.d.ts +26 -0
  47. package/dist/ui/session-bridge.js +32 -0
  48. package/package.json +2 -2
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `/session` - report this session's identity, and the two things a user does
3
+ * with it (tui-session-id-resume §3.11 / D-13 / D-14).
4
+ *
5
+ * ASCII ONLY - `src/commands/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHY THE BODY LIVES HERE AND THE REGISTRATION STAYS IN `builtins.ts`: the same
8
+ * reason `/clear` and `/compact` do. `builtins.ts` is already past the
9
+ * 1000-line guideline, and this command is a report, not a one-liner.
10
+ *
11
+ * COPYING THE RESUME COMMAND IS THE POINT, NOT A CONVENIENCE (D-14). Typing 6
12
+ * characters off a status bar is the only step of this whole feature that can
13
+ * fail for a human, and `/session copy` removes it: one keystroke, the exact
14
+ * command in the clipboard, no transcription.
15
+ */
16
+ import { sessionPathFor } from '../session/store.js';
17
+ /**
18
+ * The one path an id maps to.
19
+ *
20
+ * THROUGH `sessionPathFor` RATHER THAN A SECOND `join` (§2.2): that function is
21
+ * the single choke point where an id becomes a file name, and a report that
22
+ * built its own path would be the first thing to disagree with the writer about
23
+ * where a session lives.
24
+ */
25
+ export function sessionFilePath(id) {
26
+ return sessionPathFor(id);
27
+ }
28
+ /**
29
+ * Run `/session`, `/session copy` or `/session pin`.
30
+ *
31
+ * ONE NOTICE FOR THE WHOLE REPORT (§3.11 / R-20): six separate notices would put
32
+ * six cards in the transcript and replay them as six fragments on exit. The
33
+ * `file` line is NOT truncated even when it overflows: it is a path the user
34
+ * needs to be able to copy, and it is in the same family as `/save`'s
35
+ * `Saved session to <path>`.
36
+ */
37
+ export function runSessionCommand(ctx) {
38
+ const port = ctx.session;
39
+ if (!port) {
40
+ // HONEST RATHER THAN SILENT: a build without the feature should say so,
41
+ // because "nothing happened" reads as a bug.
42
+ ctx.notify('warn', 'This session has no id (session recording is off for this build).');
43
+ return;
44
+ }
45
+ const verb = ctx.args.trim().toLowerCase();
46
+ const identity = port.identity();
47
+ if (!identity) {
48
+ ctx.notify('warn', 'This session has no id (session recording is off for this build).');
49
+ return;
50
+ }
51
+ if (verb === 'copy') {
52
+ copyCommand(ctx, identity);
53
+ return;
54
+ }
55
+ if (verb === 'pin') {
56
+ pinSession(ctx, port, identity);
57
+ return;
58
+ }
59
+ if (verb !== '') {
60
+ // Rejected rather than ignored: a command that silently discards an argument
61
+ // is a surface that lies about what it understood.
62
+ ctx.notify('warn', `Unknown argument "${ctx.args.trim()}" - use /session [copy|pin].`);
63
+ return;
64
+ }
65
+ report(ctx, identity);
66
+ }
67
+ /** The six-line report. Pure read: no checkpoint, no toast, no dispatch. */
68
+ function report(ctx, identity) {
69
+ const resume = `aragon --resume ${identity.id}`;
70
+ const lines = [
71
+ `id ${identity.id}`,
72
+ `file ${sessionFilePath(identity.id)}`,
73
+ identity.parentId
74
+ ? `branch ${identity.parentId} (this session does not write to it)`
75
+ : 'branch (none - this session started here)',
76
+ `pinned ${identity.pinned ? 'yes' : 'no'}`,
77
+ `resume ${resume}`,
78
+ 'save as aragon --save <new-id>',
79
+ ];
80
+ ctx.notify('info', lines.join('\n'));
81
+ }
82
+ /** `/session copy` - the exact command, through the SAME door `/copy` uses. */
83
+ function copyCommand(ctx, identity) {
84
+ const text = `aragon --resume ${identity.id}`;
85
+ if (!ctx.requestCopy) {
86
+ ctx.notify('warn', 'Clipboard coordinator unavailable.');
87
+ return;
88
+ }
89
+ void ctx.requestCopy({ text, lines: 1 }).then((result) => {
90
+ if (result.status === 'confirmed' || result.status === 'sent') {
91
+ ctx.toast('info', `Copied ${text}`);
92
+ return;
93
+ }
94
+ // A failed or refused copy must not look like a successful one: the command
95
+ // is short enough to read off the report above, so naming the failure is
96
+ // enough to recover.
97
+ ctx.notify('warn', result.status === 'busy' ? 'Clipboard is busy - try /session copy again.' :
98
+ `Copy failed: ${result.reason}`);
99
+ });
100
+ }
101
+ /** `/session pin` - write once and stop `sessions prune` from ever touching it. */
102
+ function pinSession(ctx, port, identity) {
103
+ const result = port.checkpoint({ pin: true });
104
+ if (!result.ok) {
105
+ ctx.notify('warn', `Could not pin this session: ${result.message}`);
106
+ return;
107
+ }
108
+ ctx.toast('info', `Session ${identity.id} pinned: aragon sessions prune will not delete it.`);
109
+ }
@@ -228,6 +228,21 @@ export interface CliFlags {
228
228
  compaction?: boolean;
229
229
  /** `--compaction-threshold <0..1|N%>` — session override for `threshold`. */
230
230
  compactionThreshold?: string;
231
+ /**
232
+ * `--resume <id|path>` - continue a saved session under a NEW id
233
+ * (tui-session-id-resume 4).
234
+ *
235
+ * A FLAG FIELD AND NOT A CONFIG KEY (D-19). There is no persisted "always
236
+ * resume this session", and no switch that shows an id without writing it:
237
+ * that state - an id a user can copy and cannot use - is worse than having no
238
+ * id at all.
239
+ *
240
+ * Carried through `toFlags` like every other flag; a field that reaches only
241
+ * `CliFlags` is inert with no compile error and no runtime error.
242
+ */
243
+ resumeSession?: string;
244
+ /** `--save <id>` - name THIS run's persistent session and pin it. */
245
+ saveSessionAs?: string;
231
246
  }
232
247
  /**
233
248
  * Resolve the effective config by merging all layers, then derive the runtime
@@ -418,7 +418,16 @@ function planSession(options, cwd, err) {
418
418
  if (options.sessionId !== undefined) {
419
419
  // THE UPSERT. A wrapper that mints its own id must not have to ask whether
420
420
  // this is the first turn; that is the whole reason this form exists.
421
- return { id: options.sessionId, saved: readSessionFile(sessionPathFor(options.sessionId)), saving, cwd };
421
+ //
422
+ // AND IT IS A RESUME FORM, so the same write-back rule applies (tui-session-
423
+ // id-resume D-20 / I-3). When a file already exists under that id,
424
+ // `resumedSessionId` decides which id this run writes - and for a TUI
425
+ // checkpoint that answer is a NEW uuid. Without this line the wrapper-friendly
426
+ // upsert was the one remaining way to reach a TUI checkpoint and replace it,
427
+ // which is silent, unrecoverable loss of the user's live conversation.
428
+ const path = sessionPathFor(options.sessionId);
429
+ const saved = readSessionFile(path);
430
+ return { id: saved ? resumedSessionId(path, saved) : options.sessionId, saved, saving, cwd };
422
431
  }
423
432
  return { id: randomUUID(), saved: null, saving, cwd };
424
433
  }
@@ -23,7 +23,7 @@
23
23
  import type { LogConfig } from '../config/schema.js';
24
24
  import { type LogLevelName } from './levels.js';
25
25
  /** Closed set, not free text: a typo'd scope is a record nobody can filter on. */
26
- export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction';
26
+ export type LogScope = 'cli' | 'config' | 'agent' | 'tool' | 'skills' | 'llm' | 'migrate' | 'log' | 'fast' | 'history' | 'update' | 'compaction' | 'session' | 'branch';
27
27
  export interface LogRecord {
28
28
  ts: string;
29
29
  lv: LogLevelName;
@@ -0,0 +1,173 @@
1
+ /**
2
+ * What a new terminal window looks like from the outside (tui-branch-terminal-shortcut
3
+ * 3.7).
4
+ *
5
+ * ASCII ONLY - `src/proc/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * PURE, AND THAT IS THE POINT. Three platforms, at least ten terminal emulators,
8
+ * four exec-argument conventions and Windows' three layers of parsing (we -> `cmd`
9
+ * -> `PowerShell`) all collapse into "what should we spawn, with which argv". A
10
+ * command body that called `spawn('cmd', ['/c','start', ...])` inline would bury
11
+ * the only testable part of this feature - the command line itself - inside
12
+ * untestable IO. `proc/readiness.ts` and `proc/classify.ts` set the same precedent
13
+ * inside this directory.
14
+ *
15
+ * WHY SESSION CODE MAY DEPEND ON THIS MODULE: it imports nothing from
16
+ * `node:child_process`, reads no configuration and creates no process. The single
17
+ * place a process is created stays in `session/branch-launch.ts`, so `proc/` keeps
18
+ * meaning "processes" and `session/` keeps meaning "sessions".
19
+ *
20
+ * EVERY RUNG CARRIES ITS OWN WORKING DIRECTORY (D-16 / D-23 / I-9). This is the
21
+ * one thing a reviewer will assume is free and is not: `spawn({ cwd })` decides the
22
+ * cwd of the process WE start, and for `wt -w 0 nt` (a new tab in an already
23
+ * running Windows Terminal) or `osascript` (a new Terminal.app window) that
24
+ * process merely forwards the request - the window opens in the terminal's own
25
+ * default directory. So `wt` passes `-d`, and both script rungs start with
26
+ * `cd <cwd> &&`. `--resume` does not restore a cwd, so a window that opens in the
27
+ * wrong place can never be corrected from inside the branch.
28
+ */
29
+ /** The three ladders, collapsed from `NodeJS.Platform` (win32 / darwin / everything else). */
30
+ export type TerminalPlatform = 'win32' | 'darwin' | 'posix';
31
+ export interface ShellInvocation {
32
+ /** Executable: an absolute path or a name resolved through PATH. */
33
+ path: string;
34
+ /** Which command syntax the aragon command has to be encoded for. */
35
+ kind: 'powershell' | 'posix';
36
+ }
37
+ export interface TerminalLaunchPlan {
38
+ /** The program we spawn. */
39
+ file: string;
40
+ args: string[];
41
+ /** True only for the `cmd` rung, whose command line `cmd.exe` must see verbatim. */
42
+ windowsVerbatimArguments: boolean;
43
+ /** Ladder name, for the log and for the success toast's debug detail. */
44
+ rung: string;
45
+ }
46
+ export type TerminalPlanResult = {
47
+ ok: true;
48
+ plans: TerminalLaunchPlan[];
49
+ shell: ShellInvocation;
50
+ manual: string;
51
+ } | {
52
+ ok: false;
53
+ reason: 'no-launcher';
54
+ shell: ShellInvocation;
55
+ manual: string;
56
+ };
57
+ export interface TerminalPlanInput {
58
+ platform: NodeJS.Platform;
59
+ env: Record<string, string | undefined>;
60
+ pathDirs: readonly string[];
61
+ isExecutable?: (name: string) => boolean;
62
+ shell: ShellInvocation;
63
+ /** The aragon command's argv pieces (`session/branch-invocation.ts`'s product). */
64
+ command: readonly string[];
65
+ /** New window title, ASCII, already truncated to the cell budget. */
66
+ title: string;
67
+ cwd: string;
68
+ /**
69
+ * `renderManualCommand(command, platform)`'s product: two lines on win32
70
+ * (`PowerShell:` / `cmd.exe :`), one line everywhere else.
71
+ */
72
+ manual: string;
73
+ }
74
+ /** The env var that overrides the shell the new window runs (D-7). */
75
+ export declare const ARAGON_BRANCH_SHELL_ENV = "ARAGON_BRANCH_SHELL";
76
+ /**
77
+ * Build the ladder for this platform, most-preferred rung first.
78
+ *
79
+ * The return value is a LIST because the failures this feature can actually hit
80
+ * are synchronous argument rejections (ENOENT on `wt.exe`, no X display), and a
81
+ * list lets `branch-launch.ts` walk down it without knowing anything about
82
+ * terminals. Rung COUNT is bounded by the caller (`BRANCH_TERMINAL_LIMITS.maxRungs`),
83
+ * so there is exactly one place that number lives.
84
+ */
85
+ export declare function planTerminalLaunch(input: TerminalPlanInput): TerminalPlanResult;
86
+ /**
87
+ * The PowerShell statement that runs the aragon command (D-1).
88
+ *
89
+ * THE LEADING `&` IS NOT DECORATION. In command position PowerShell reads a quoted
90
+ * string as an EXPRESSION, so `'C:\Program Files\nodejs\node.exe' <script>` is a
91
+ * parse error - "unexpected token ..." at column 36 - and the child is never
92
+ * created, while the parent still believes the window opened (K-1: `wt`/`start`
93
+ * forward and exit). That is exactly the report this branch exists to fix.
94
+ * `&` is the call operator: it takes the following word as a command name whatever
95
+ * its shape, and it does not change name resolution (`& 'npm.cmd'` still finds
96
+ * `npm` through PATH/PATHEXT, appendix B-8).
97
+ *
98
+ * `posixScript` MUST NOT be given the same treatment: in sh `&` means run in the
99
+ * background, so the window would reach the trailing `exec <shell> -i` immediately
100
+ * and the branch would start behind a prompt that looks idle (I-3 / K-1).
101
+ */
102
+ export declare function powerShellScript(parts: readonly string[]): string;
103
+ /** Bare words stay bare; everything else becomes a single-quoted literal. */
104
+ export declare function powerShellQuote(value: string): string;
105
+ /**
106
+ * `-EncodedCommand` takes base64 of UTF-16LE, which needs no shell quoting at all.
107
+ *
108
+ * The alphabet is `A-Za-z0-9+/=` - no spaces, no quotes, no `cmd` metacharacters -
109
+ * so the blob survives all three of Windows' parsers as an ordinary word. This is
110
+ * the same trick `ui/win-vt-force.ts` uses, for the same reason: encode the fragile
111
+ * thing into a form nothing can argue with, at construction time. Our command line
112
+ * necessarily contains spaced paths (`C:\Program Files\nodejs\node.exe`), so the
113
+ * fragile thing is not hypothetical.
114
+ */
115
+ export declare function encodePowerShellCommand(script: string): string;
116
+ /** POSIX single-quote escaping: `'` becomes `'\''`. */
117
+ export declare function posixQuote(value: string): string;
118
+ export declare function posixScript(parts: readonly string[]): string;
119
+ /** AppleScript string literals only need `\` and `"` escaped. */
120
+ export declare function escapeAppleScriptText(value: string): string;
121
+ /**
122
+ * Win32 argv quoting for the `cmd` rung's title and shell path: quote when it
123
+ * contains whitespace, otherwise leave it alone.
124
+ *
125
+ * THE INPUT DOMAIN EXCLUDES `"` BY CONSTRUCTION (R-17/T-48): Windows paths cannot
126
+ * contain it, and the title is `aragon <id>` where an id is `[A-Za-z0-9._-]`. If a
127
+ * future source can contain a quote, this must switch to `cmd`'s rule (`""`) and
128
+ * not to the POSIX `\"` written here - the two are not interchangeable.
129
+ */
130
+ export declare function quoteWin32Arg(value: string): string;
131
+ /** PATH entries for `name` probing: both spellings on Windows, `:` on POSIX. */
132
+ export declare function pathDirsOf(env: Record<string, string | undefined>, platform: NodeJS.Platform): string[];
133
+ /**
134
+ * The default probe: is this a file we could execute?
135
+ *
136
+ * An absolute or separator-bearing value is stat'd directly (that is the
137
+ * `ARAGON_BRANCH_SHELL=/usr/local/bin/fish` case); a bare name is looked up in the
138
+ * PATH directories, trying `name` and - on Windows - `name + '.exe'`, because a
139
+ * user typing `pwsh` and a probe looking for `pwsh.exe` are the same request.
140
+ *
141
+ * EVERY EXCEPTION RETURNS FALSE: one failed stat means "this candidate is not
142
+ * here", not "the feature is broken", and a missing terminal must degrade to the
143
+ * manual command rather than throw out of a key handler.
144
+ */
145
+ export declare function defaultIsExecutable(platform: NodeJS.Platform, pathDirs?: readonly string[]): (name: string) => boolean;
146
+ /**
147
+ * Which shell the new window runs (D-7), first match wins.
148
+ *
149
+ * The requirement says "PowerShell / bash", and both platforms have two answers
150
+ * worth distinguishing: PowerShell 7 (`pwsh.exe`) when the user installed it,
151
+ * Windows PowerShell 5.1 otherwise; `$SHELL` when the user's login shell is not
152
+ * bash, then bash, then `/bin/sh` as the one path that exists everywhere.
153
+ * `ARAGON_BRANCH_SHELL` is the escape hatch - an ENV VALUE, not a config key
154
+ * (D-17): a "Ctrl+L opens no terminal" switch would create the one state this
155
+ * feature must never have.
156
+ */
157
+ export declare function resolveShell(input: {
158
+ platform: NodeJS.Platform;
159
+ env: Record<string, string | undefined>;
160
+ isExecutable?: (name: string) => boolean;
161
+ pathDirs: readonly string[];
162
+ }): ShellInvocation;
163
+ /**
164
+ * Fit a window title into `maxCells` columns (R-7 / T-47).
165
+ *
166
+ * THE TITLE IS THE ONLY THING THIS TOUCHES. An id can be 64 characters long, so
167
+ * `aragon <id>` exceeds the 60-cell budget by construction for the longest ids -
168
+ * and truncating the id anywhere else (the command line, `--resume`) would produce
169
+ * a branch that cannot find its own parent. Code points, not bytes: the title is
170
+ * ASCII today, and a future non-ASCII title must not be cut through a surrogate
171
+ * pair.
172
+ */
173
+ export declare function truncateTitle(title: string, maxCells: number): string;