@aragon-agent/cli 0.5.0 → 0.5.2

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 (102) hide show
  1. package/CHANGELOG.md +158 -0
  2. package/README.md +254 -7
  3. package/dist/agent/agent-mode.d.ts +53 -0
  4. package/dist/agent/agent-mode.js +69 -0
  5. package/dist/agent/controller.d.ts +69 -0
  6. package/dist/agent/controller.js +154 -1
  7. package/dist/agent/plan-prompt.d.ts +18 -0
  8. package/dist/agent/plan-prompt.js +93 -0
  9. package/dist/agent/reducer.d.ts +14 -1
  10. package/dist/agent/reducer.js +9 -0
  11. package/dist/agent/system-prompt.d.ts +15 -0
  12. package/dist/agent/system-prompt.js +8 -0
  13. package/dist/cli.d.ts +1 -1
  14. package/dist/cli.js +247 -23
  15. package/dist/commands/builtins.js +60 -0
  16. package/dist/commands/registry.d.ts +17 -0
  17. package/dist/config/app-paths.d.ts +71 -4
  18. package/dist/config/app-paths.js +139 -8
  19. package/dist/config/cli-commands.d.ts +43 -0
  20. package/dist/config/cli-commands.js +196 -0
  21. package/dist/config/env.d.ts +7 -1
  22. package/dist/config/env.js +53 -1
  23. package/dist/config/load.d.ts +10 -0
  24. package/dist/config/load.js +82 -2
  25. package/dist/config/migrate-home.d.ts +63 -0
  26. package/dist/config/migrate-home.js +165 -0
  27. package/dist/config/migrate-legacy-state.d.ts +10 -0
  28. package/dist/config/migrate-legacy-state.js +15 -6
  29. package/dist/config/schema.d.ts +81 -0
  30. package/dist/config/schema.js +85 -0
  31. package/dist/config/store.d.ts +30 -12
  32. package/dist/config/store.js +47 -35
  33. package/dist/input/mouse-events.d.ts +61 -0
  34. package/dist/input/mouse-events.js +153 -0
  35. package/dist/input/stdin-mouse-filter.d.ts +42 -0
  36. package/dist/input/stdin-mouse-filter.js +153 -0
  37. package/dist/logging/cli-commands.d.ts +25 -0
  38. package/dist/logging/cli-commands.js +221 -0
  39. package/dist/logging/file-sink.d.ts +114 -0
  40. package/dist/logging/file-sink.js +326 -0
  41. package/dist/logging/install.d.ts +163 -0
  42. package/dist/logging/install.js +302 -0
  43. package/dist/logging/levels.d.ts +40 -0
  44. package/dist/logging/levels.js +59 -0
  45. package/dist/logging/logger.d.ts +110 -0
  46. package/dist/logging/logger.js +237 -0
  47. package/dist/logging/redact.d.ts +28 -0
  48. package/dist/logging/redact.js +90 -0
  49. package/dist/logging/secret-registry.d.ts +30 -0
  50. package/dist/logging/secret-registry.js +55 -0
  51. package/dist/skills/paths.d.ts +2 -4
  52. package/dist/skills/paths.js +7 -9
  53. package/dist/tools/human-input.d.ts +179 -0
  54. package/dist/tools/human-input.js +227 -0
  55. package/dist/tools/index.d.ts +35 -3
  56. package/dist/tools/index.js +96 -9
  57. package/dist/tools/plan-tools.d.ts +64 -0
  58. package/dist/tools/plan-tools.js +198 -0
  59. package/dist/ui/App.d.ts +12 -1
  60. package/dist/ui/App.js +223 -17
  61. package/dist/ui/Composer.d.ts +9 -1
  62. package/dist/ui/Composer.js +26 -7
  63. package/dist/ui/ModeChip.d.ts +29 -0
  64. package/dist/ui/ModeChip.js +15 -0
  65. package/dist/ui/PromptInput.d.ts +22 -1
  66. package/dist/ui/PromptInput.js +49 -2
  67. package/dist/ui/StatusBar.d.ts +15 -0
  68. package/dist/ui/StatusBar.js +10 -2
  69. package/dist/ui/entries/ToolPreview.d.ts +2 -0
  70. package/dist/ui/entries/ToolPreview.js +64 -0
  71. package/dist/ui/glyphs.d.ts +21 -0
  72. package/dist/ui/glyphs.js +12 -0
  73. package/dist/ui/layout/AppShell.d.ts +10 -1
  74. package/dist/ui/layout/AppShell.js +38 -3
  75. package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
  76. package/dist/ui/layout/ScrollIndicator.js +15 -0
  77. package/dist/ui/layout/ScrollViewport.d.ts +8 -2
  78. package/dist/ui/layout/ScrollViewport.js +24 -4
  79. package/dist/ui/layout/regions.d.ts +55 -0
  80. package/dist/ui/layout/regions.js +52 -0
  81. package/dist/ui/layout/scroll-indicator.d.ts +27 -0
  82. package/dist/ui/layout/scroll-indicator.js +35 -0
  83. package/dist/ui/layout/scroll.d.ts +9 -0
  84. package/dist/ui/layout/scroll.js +23 -0
  85. package/dist/ui/layout/wrap-rows.d.ts +24 -0
  86. package/dist/ui/layout/wrap-rows.js +74 -0
  87. package/dist/ui/overlays/HelpOverlay.js +15 -0
  88. package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
  89. package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
  90. package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
  91. package/dist/ui/overlays/QuestionOverlay.js +193 -0
  92. package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
  93. package/dist/ui/overlays/SettingsScreen.js +8 -1
  94. package/dist/ui/screen.d.ts +14 -1
  95. package/dist/ui/screen.js +37 -3
  96. package/dist/ui/use-startup-notices.d.ts +25 -0
  97. package/dist/ui/use-startup-notices.js +64 -0
  98. package/dist/ui/use-wheel-routing.d.ts +68 -0
  99. package/dist/ui/use-wheel-routing.js +139 -0
  100. package/package.json +3 -2
  101. package/runtime/insecure-tls-warning.cjs +62 -0
  102. package/runtime/insecure-tls-warning.d.cts +5 -0
@@ -0,0 +1,63 @@
1
+ /**
2
+ * One-shot migration of the user's on-disk state from the `env-paths`
3
+ * directories into `~/.aragon-agent/`.
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS EXISTS: up to 0.5.0 the config lived in
7
+ * `%APPDATA%\aragon-agent-nodejs\Config` and the sessions and skills in
8
+ * `%LOCALAPPDATA%\aragon-agent-nodejs\Data`. Moving the root without moving the
9
+ * contents would present to every existing user as a first run: no API key, no
10
+ * saved sessions, no installed skills — with all of it still on disk somewhere
11
+ * nobody thinks to look.
12
+ * ══════════════════════════════════════════════════════════════════════════
13
+ *
14
+ * ORDER MATTERS AND IS NOT INTERCHANGEABLE. `migrateLegacyState()` (chain step
15
+ * ①, pre-rename tree -> current tree) MUST run first. Run this one first and it
16
+ * finds the `aragon-agent` env-paths tree empty and does nothing; ① then moves
17
+ * the 0.4.x data into a location no code reads any more. Because the idempotence
18
+ * keys below are per-artifact, `sessions` and `skills` would still be picked up
19
+ * on the NEXT launch — but `config.json` would not: by then the user has re-
20
+ * entered their key, the target file exists, and the original is stranded.
21
+ *
22
+ * THIS FUNCTION NEVER THROWS, for the reason spelled out in
23
+ * `migrate-legacy-state.ts`: a user who has to re-enter an API key is
24
+ * inconvenienced; a user whose CLI will not start is blocked.
25
+ *
26
+ * THE IDEMPOTENCE KEY IS PER-ARTIFACT — "does this target already exist" — and
27
+ * deliberately NOT "does the home root exist". The logger creates `<home>/logs`
28
+ * and `skills/usage.ts` creates `<home>` itself, both of them possibly in this
29
+ * same process; keying on the root would let a half-finished migration be
30
+ * skipped forever on the next launch.
31
+ */
32
+ /** Filesystem surface, so tests never touch a real user directory. */
33
+ export interface HomeMigrationFsPort {
34
+ existsSync(path: string): boolean;
35
+ mkdirSync(path: string, options: {
36
+ recursive: true;
37
+ }): void;
38
+ renameSync(from: string, to: string): void;
39
+ cpSync(from: string, to: string, options: {
40
+ recursive: true;
41
+ }): void;
42
+ writeFileSync(path: string, data: string): void;
43
+ }
44
+ export declare const nodeHomeFsPort: HomeMigrationFsPort;
45
+ export interface HomeMigrationResult {
46
+ mode: 'none' | 'renamed' | 'copied' | 'mixed';
47
+ /** Artifact names that actually moved (`config.json`, `sessions`, …). */
48
+ moved: string[];
49
+ /** Populated instead of throwing. */
50
+ error?: string;
51
+ }
52
+ export declare function migrateToHome(fs?: HomeMigrationFsPort): HomeMigrationResult;
53
+ /** Path of the breadcrumb, for `aragon config home`'s "previous location" line. */
54
+ export declare function getHomeMigrationBreadcrumbPath(): string;
55
+ /**
56
+ * The one-line notice for a migration that did something.
57
+ *
58
+ * Callers MUST write this to stderr: `aragon -p "..." > out.txt` yields model
59
+ * output and nothing else, and one stray line here breaks every scripted use.
60
+ * ASCII-only with `->` rather than an arrow glyph — this prints before any
61
+ * terminal capability probe has run.
62
+ */
63
+ export declare function formatHomeMigrationNotice(result: HomeMigrationResult): string | null;
@@ -0,0 +1,165 @@
1
+ /**
2
+ * One-shot migration of the user's on-disk state from the `env-paths`
3
+ * directories into `~/.aragon-agent/`.
4
+ *
5
+ * ══════════════════════════════════════════════════════════════════════════
6
+ * WHY THIS EXISTS: up to 0.5.0 the config lived in
7
+ * `%APPDATA%\aragon-agent-nodejs\Config` and the sessions and skills in
8
+ * `%LOCALAPPDATA%\aragon-agent-nodejs\Data`. Moving the root without moving the
9
+ * contents would present to every existing user as a first run: no API key, no
10
+ * saved sessions, no installed skills — with all of it still on disk somewhere
11
+ * nobody thinks to look.
12
+ * ══════════════════════════════════════════════════════════════════════════
13
+ *
14
+ * ORDER MATTERS AND IS NOT INTERCHANGEABLE. `migrateLegacyState()` (chain step
15
+ * ①, pre-rename tree -> current tree) MUST run first. Run this one first and it
16
+ * finds the `aragon-agent` env-paths tree empty and does nothing; ① then moves
17
+ * the 0.4.x data into a location no code reads any more. Because the idempotence
18
+ * keys below are per-artifact, `sessions` and `skills` would still be picked up
19
+ * on the NEXT launch — but `config.json` would not: by then the user has re-
20
+ * entered their key, the target file exists, and the original is stranded.
21
+ *
22
+ * THIS FUNCTION NEVER THROWS, for the reason spelled out in
23
+ * `migrate-legacy-state.ts`: a user who has to re-enter an API key is
24
+ * inconvenienced; a user whose CLI will not start is blocked.
25
+ *
26
+ * THE IDEMPOTENCE KEY IS PER-ARTIFACT — "does this target already exist" — and
27
+ * deliberately NOT "does the home root exist". The logger creates `<home>/logs`
28
+ * and `skills/usage.ts` creates `<home>` itself, both of them possibly in this
29
+ * same process; keying on the root would let a half-finished migration be
30
+ * skipped forever on the next launch.
31
+ */
32
+ import { cpSync, existsSync, mkdirSync, renameSync, writeFileSync } from 'node:fs';
33
+ import { dirname, join } from 'node:path';
34
+ import { getHomeRoot, legacyEnvPaths } from './app-paths.js';
35
+ import { LEGACY_MIGRATION_BREADCRUMB_FILENAME } from './migrate-legacy-state.js';
36
+ export const nodeHomeFsPort = {
37
+ existsSync,
38
+ mkdirSync: (path, options) => {
39
+ mkdirSync(path, options);
40
+ },
41
+ renameSync,
42
+ cpSync,
43
+ writeFileSync: (path, data) => writeFileSync(path, data, 'utf-8'),
44
+ };
45
+ const BREADCRUMB_FILENAME = '.migrated-from-env-paths';
46
+ function artifacts(homeRoot) {
47
+ const config = legacyEnvPaths.config;
48
+ const data = legacyEnvPaths.data;
49
+ return [
50
+ {
51
+ name: 'config.json',
52
+ from: join(config, 'config.json'),
53
+ to: join(homeRoot, 'config.json'),
54
+ copy: true,
55
+ },
56
+ { name: 'sessions', from: join(data, 'sessions'), to: join(homeRoot, 'sessions'), copy: false },
57
+ { name: 'skills', from: join(data, 'skills'), to: join(homeRoot, 'skills'), copy: false },
58
+ {
59
+ name: 'skill-usage.json',
60
+ from: join(data, 'skill-usage.json'),
61
+ to: join(homeRoot, 'skill-usage.json'),
62
+ copy: false,
63
+ },
64
+ {
65
+ name: LEGACY_MIGRATION_BREADCRUMB_FILENAME,
66
+ from: join(config, LEGACY_MIGRATION_BREADCRUMB_FILENAME),
67
+ to: join(homeRoot, LEGACY_MIGRATION_BREADCRUMB_FILENAME),
68
+ copy: false,
69
+ },
70
+ ];
71
+ }
72
+ export function migrateToHome(fs = nodeHomeFsPort) {
73
+ const result = { mode: 'none', moved: [] };
74
+ try {
75
+ const homeRoot = getHomeRoot();
76
+ let renamed = false;
77
+ let copied = false;
78
+ for (const artifact of artifacts(homeRoot)) {
79
+ // Already migrated, or built by hand. Either way: never overwrite.
80
+ if (fs.existsSync(artifact.to))
81
+ continue;
82
+ if (!fs.existsSync(artifact.from))
83
+ continue;
84
+ fs.mkdirSync(dirname(artifact.to), { recursive: true });
85
+ if (artifact.copy) {
86
+ fs.cpSync(artifact.from, artifact.to, { recursive: true });
87
+ copied = true;
88
+ }
89
+ else if (moveOrCopy(fs, artifact)) {
90
+ renamed = true;
91
+ }
92
+ else {
93
+ copied = true;
94
+ }
95
+ result.moved.push(artifact.name);
96
+ }
97
+ if (result.moved.length === 0)
98
+ return result;
99
+ result.mode = renamed && copied ? 'mixed' : renamed ? 'renamed' : 'copied';
100
+ writeBreadcrumb(fs, homeRoot, result);
101
+ return result;
102
+ }
103
+ catch (err) {
104
+ return { mode: 'none', moved: [], error: err instanceof Error ? err.message : String(err) };
105
+ }
106
+ }
107
+ /**
108
+ * Rename, falling back to a copy that KEEPS the source. Returns whether the
109
+ * rename succeeded.
110
+ *
111
+ * On Windows `%APPDATA%` and `%LOCALAPPDATA%` can sit on different volumes, so
112
+ * `EXDEV` here is ordinary rather than exceptional; antivirus contributes
113
+ * `EPERM`. A half-written copy next to a deleted original is the only outcome
114
+ * worse than not migrating at all, which is why the source stays.
115
+ */
116
+ function moveOrCopy(fs, artifact) {
117
+ try {
118
+ fs.renameSync(artifact.from, artifact.to);
119
+ return true;
120
+ }
121
+ catch {
122
+ fs.cpSync(artifact.from, artifact.to, { recursive: true });
123
+ return false;
124
+ }
125
+ }
126
+ /**
127
+ * Support-diagnosis breadcrumb, and deliberately NOT the idempotence key.
128
+ *
129
+ * It swallows its own failures for the same reason the brand migration's does:
130
+ * letting an ENOENT here reach the caller's outer catch would report
131
+ * `mode: 'none'` for a migration that actually moved the user's data — telling
132
+ * support the exact opposite of what happened.
133
+ */
134
+ function writeBreadcrumb(fs, homeRoot, result) {
135
+ const payload = {
136
+ migratedAt: new Date().toISOString(),
137
+ mode: result.mode,
138
+ from: { config: legacyEnvPaths.config, data: legacyEnvPaths.data },
139
+ moved: result.moved,
140
+ };
141
+ try {
142
+ fs.mkdirSync(homeRoot, { recursive: true });
143
+ fs.writeFileSync(join(homeRoot, BREADCRUMB_FILENAME), `${JSON.stringify(payload, null, 2)}\n`);
144
+ }
145
+ catch {
146
+ // Intentionally ignored — see the note above.
147
+ }
148
+ }
149
+ /** Path of the breadcrumb, for `aragon config home`'s "previous location" line. */
150
+ export function getHomeMigrationBreadcrumbPath() {
151
+ return join(getHomeRoot(), BREADCRUMB_FILENAME);
152
+ }
153
+ /**
154
+ * The one-line notice for a migration that did something.
155
+ *
156
+ * Callers MUST write this to stderr: `aragon -p "..." > out.txt` yields model
157
+ * output and nothing else, and one stray line here breaks every scripted use.
158
+ * ASCII-only with `->` rather than an arrow glyph — this prints before any
159
+ * terminal capability probe has run.
160
+ */
161
+ export function formatHomeMigrationNotice(result) {
162
+ if (result.mode === 'none')
163
+ return null;
164
+ return `moved settings to ${getHomeRoot()} (${result.moved.join(', ')})`;
165
+ }
@@ -20,6 +20,15 @@
20
20
  * has to re-enter an API key is inconvenienced, a user whose CLI refuses to
21
21
  * launch is blocked. Every failure path degrades to `{ mode: 'none', error }`.
22
22
  *
23
+ * CHAIN POSITION: this is step ① of two. `migrateToHome()` runs immediately
24
+ * after it and moves the same data on to `~/.aragon-agent/`. The order is not
25
+ * interchangeable — see the header of `migrate-home.ts` for what running them
26
+ * the other way round strands, and why the damage is silent.
27
+ *
28
+ * The directories this module reads and writes are the `env-paths` roots, which
29
+ * after `migrateToHome()` hold nothing the CLI reads. That is why they are
30
+ * imported here as `legacyEnvPaths`: no other module may use them.
31
+ *
23
32
  * IDEMPOTENCE KEY is "does the new directory already exist" — not the
24
33
  * breadcrumb file, which is written for support diagnosis only. Two cold starts
25
34
  * racing each other therefore both observe `existsSync(next) === false` and
@@ -63,6 +72,7 @@ export interface MigrationResult {
63
72
  /** Populated instead of throwing. */
64
73
  error?: string;
65
74
  }
75
+ export declare const LEGACY_MIGRATION_BREADCRUMB_FILENAME = ".migrated-from-argon-agent";
66
76
  export declare function migrateLegacyState(fs?: MigrationFsPort): MigrationResult;
67
77
  /**
68
78
  * The one-line notice for a migration that did something.
@@ -20,6 +20,15 @@
20
20
  * has to re-enter an API key is inconvenienced, a user whose CLI refuses to
21
21
  * launch is blocked. Every failure path degrades to `{ mode: 'none', error }`.
22
22
  *
23
+ * CHAIN POSITION: this is step ① of two. `migrateToHome()` runs immediately
24
+ * after it and moves the same data on to `~/.aragon-agent/`. The order is not
25
+ * interchangeable — see the header of `migrate-home.ts` for what running them
26
+ * the other way round strands, and why the damage is silent.
27
+ *
28
+ * The directories this module reads and writes are the `env-paths` roots, which
29
+ * after `migrateToHome()` hold nothing the CLI reads. That is why they are
30
+ * imported here as `legacyEnvPaths`: no other module may use them.
31
+ *
23
32
  * IDEMPOTENCE KEY is "does the new directory already exist" — not the
24
33
  * breadcrumb file, which is written for support diagnosis only. Two cold starts
25
34
  * racing each other therefore both observe `existsSync(next) === false` and
@@ -33,7 +42,7 @@
33
42
  import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync, writeFileSync, } from 'node:fs';
34
43
  import { dirname, join } from 'node:path';
35
44
  import envPaths from 'env-paths';
36
- import { appPaths } from './app-paths.js';
45
+ import { legacyEnvPaths as envPathsRoots } from './app-paths.js';
37
46
  /**
38
47
  * The pre-rename `env-paths` application name. Deliberately declared here and
39
48
  * not alongside `APP_NAME`: this module is the only consumer, and it is the
@@ -55,7 +64,7 @@ export const nodeFsPort = {
55
64
  const LEGACY_MANIFEST_FILENAME = '.argon-skill.json';
56
65
  const MANIFEST_FILENAME = '.aragon-skill.json';
57
66
  const LEGACY_LOCK_FILENAME = '.argon-skills.lock';
58
- const BREADCRUMB_FILENAME = '.migrated-from-argon-agent';
67
+ export const LEGACY_MIGRATION_BREADCRUMB_FILENAME = '.migrated-from-argon-agent';
59
68
  /**
60
69
  * The roots the CLI actually uses. `envPaths` also exposes `cache`, `log` and
61
70
  * `temp`; none of them hold anything the user would miss, and migrating a
@@ -70,7 +79,7 @@ export function migrateLegacyState(fs = nodeFsPort) {
70
79
  const from = {};
71
80
  for (const root of ROOTS) {
72
81
  const legacy = legacyAppPaths[root];
73
- const next = appPaths[root];
82
+ const next = envPathsRoots[root];
74
83
  // Already migrated, or the user built the new directory by hand. Either
75
84
  // way: do not migrate, and above all do not overwrite.
76
85
  if (fs.existsSync(next))
@@ -95,7 +104,7 @@ export function migrateLegacyState(fs = nodeFsPort) {
95
104
  return result;
96
105
  result.mode = copied ? 'copied' : 'renamed';
97
106
  if (result.movedRoots.includes('data')) {
98
- result.manifestsRenamed = renameSkillManifests(fs, join(appPaths.data, 'skills'));
107
+ result.manifestsRenamed = renameSkillManifests(fs, join(envPathsRoots.data, 'skills'));
99
108
  }
100
109
  writeBreadcrumb(fs, result, from);
101
110
  return result;
@@ -155,8 +164,8 @@ function writeBreadcrumb(fs, result, from) {
155
164
  manifestsRenamed: result.manifestsRenamed,
156
165
  };
157
166
  try {
158
- fs.mkdirSync(appPaths.config, { recursive: true });
159
- fs.writeFileSync(join(appPaths.config, BREADCRUMB_FILENAME), `${JSON.stringify(payload, null, 2)}\n`);
167
+ fs.mkdirSync(envPathsRoots.config, { recursive: true });
168
+ fs.writeFileSync(join(envPathsRoots.config, LEGACY_MIGRATION_BREADCRUMB_FILENAME), `${JSON.stringify(payload, null, 2)}\n`);
160
169
  }
161
170
  catch {
162
171
  // Intentionally ignored — see the note above.
@@ -6,6 +6,7 @@
6
6
  * invariant).
7
7
  */
8
8
  import type { ThinkingLevel } from '@aragon-agent/core';
9
+ import { type LogLevelName } from '../logging/levels.js';
9
10
  /**
10
11
  * `ModelRegistry` seeds builtin lists for five providers, but `initProviders()`
11
12
  * registers only three adapters. Running a model whose provider has no adapter
@@ -67,6 +68,16 @@ export declare const DEFAULT_TRANSCRIPT_WINDOW = 300;
67
68
  export declare const MIN_TRANSCRIPT_WINDOW = 50;
68
69
  export declare const MAX_TRANSCRIPT_WINDOW = 2000;
69
70
  export declare function clampTranscriptWindow(v: unknown, fallback: number): number;
71
+ /** `ask_user` calls allowed per user turn. */
72
+ export declare const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
73
+ /**
74
+ * Ceiling on ONE human wait, in ms. Thirty minutes is far outside "the user
75
+ * went to get coffee", but a ceiling has to exist: an unattended terminal must
76
+ * not wedge a CI job or a stale ssh session forever.
77
+ */
78
+ export declare const DEFAULT_PLAN_HUMAN_TIMEOUT_MS = 1800000;
79
+ export declare function clampAskRounds(v: unknown, fallback: number): number;
80
+ export declare function clampHumanTimeout(v: unknown, fallback: number): number;
70
81
  export interface SkillsConfig {
71
82
  enabled: boolean;
72
83
  /** Skill names the user has switched off. */
@@ -149,6 +160,36 @@ export interface SkillsRuntimeOptions {
149
160
  toolPolicy?: SkillsToolPolicyMode;
150
161
  }
151
162
  export declare const DEFAULT_SKILLS_RUNTIME: SkillsRuntimeOptions;
163
+ export interface LogConfig {
164
+ level: LogLevelName;
165
+ /** Whether records reach a file at all. */
166
+ toFile: boolean;
167
+ /** Absolute log directory; empty string means `<home>/logs`. */
168
+ dir: string;
169
+ /** Rotate a file once it would exceed this many bytes. */
170
+ maxFileBytes: number;
171
+ /** Files kept in the log directory; the oldest beyond this are deleted. */
172
+ maxFiles: number;
173
+ /**
174
+ * Run every record through the redaction pipeline. The ONE switch a user can
175
+ * flip to put credentials on disk, which is why turning it off prints a
176
+ * warning and has no settings-screen entry (§4.3).
177
+ */
178
+ redactSecrets: boolean;
179
+ /** Truncation length for user content at `debug`; `trace` is never truncated. */
180
+ previewChars: number;
181
+ }
182
+ export declare const DEFAULT_LOG_CONFIG: LogConfig;
183
+ /**
184
+ * THE single gate for every log-config read AND write, exactly like
185
+ * `clampSkillsConfig`: hardening only the read path leaves a bad value on disk
186
+ * that reverts on every launch, which presents as "my setting won't stick".
187
+ *
188
+ * `previewChars` uses its own coercion because `coercePositiveInt` rejects `0`,
189
+ * and `0` is a legitimate value here — it means "record that content existed,
190
+ * but none of it".
191
+ */
192
+ export declare function clampLogConfig(raw: unknown): LogConfig;
152
193
  export interface PersistedConfig {
153
194
  version: number;
154
195
  provider: string;
@@ -176,10 +217,33 @@ export interface PersistedConfig {
176
217
  density: DensityMode;
177
218
  /** Show the composer hint row at all. */
178
219
  hints: boolean;
220
+ /**
221
+ * Enable SGR mouse reporting in full-screen mode so the wheel scrolls the
222
+ * region under the pointer (mouse-wheel-region-routing §5.7).
223
+ *
224
+ * A PLAIN BOOLEAN, not a `fullscreen`-style tri-state: there are no
225
+ * heuristics for an explicit `true` to override (D-10).
226
+ */
227
+ mouse: boolean;
228
+ /**
229
+ * Whether the one-time "hold Shift to select text" notice has been shown
230
+ * (§6.1). Bookkeeping for a one-shot notice, NOT a preference — hence no
231
+ * flag, no env var and no `config set` key. Giving it a user-facing channel
232
+ * would invite it to be treated as one.
233
+ */
234
+ mouseNoticeSeen: boolean;
179
235
  /** Lifetime submit count driving the progressive-disclosure fade (§4.6). */
180
236
  submitCount: number;
237
+ /** Start every session in PLAN mode (plan-mode §4.4). */
238
+ planModeDefault: boolean;
239
+ /** `ask_user` rounds allowed per user turn; clamped to [1, 10]. */
240
+ planModeMaxAskRounds: number;
241
+ /** Ceiling on a single human wait in ms; clamped to [60_000, 7_200_000]. */
242
+ planModeHumanTimeoutMs: number;
181
243
  /** Skill system settings. The SECOND nested object in this file after apiKeys. */
182
244
  skills: SkillsConfig;
245
+ /** Logging settings. The THIRD nested object; scalars only (see LogConfig). */
246
+ log: LogConfig;
183
247
  }
184
248
  export declare const DEFAULT_CONFIG: PersistedConfig;
185
249
  export interface CliConfig {
@@ -214,10 +278,27 @@ export interface CliConfig {
214
278
  promptHistory: string[];
215
279
  density: DensityMode;
216
280
  hints: boolean;
281
+ /**
282
+ * Whether this session may enable SGR mouse reporting (full-screen + TTY
283
+ * only). Resolved by `resolveMouse` in `load.ts`.
284
+ */
285
+ mouse: boolean;
217
286
  submitCount: number;
287
+ /**
288
+ * Whether this session opens in PLAN mode.
289
+ *
290
+ * Named for what it DOES rather than mirroring the persisted key: after
291
+ * resolution it is a resolved starting posture, not a preference, and the
292
+ * live mode from here on belongs to the controller.
293
+ */
294
+ startInPlanMode: boolean;
295
+ planModeMaxAskRounds: number;
296
+ planModeHumanTimeoutMs: number;
218
297
  skills: SkillsConfig;
219
298
  /** Flag/env-only skill switches for this run (§7.5). Never persisted. */
220
299
  skillsRuntime: SkillsRuntimeOptions;
300
+ /** Resolved logging settings; the authoritative input to `Logger.reconfigure`. */
301
+ log: LogConfig;
221
302
  /** Working directory for tools (default `process.cwd()`, overridable). */
222
303
  cwd: string;
223
304
  /** Whether ANSI color is enabled. */
@@ -5,6 +5,8 @@
5
5
  * See spec §3.5 (resolution pipeline), §6.1 (config file), and §3.8 (timeout
6
6
  * invariant).
7
7
  */
8
+ import { isAbsolute } from 'node:path';
9
+ import { clampLogLevel } from '../logging/levels.js';
8
10
  // ---------------------------------------------------------------------------
9
11
  // Provider set — only the adapter-backed providers are usable at run time (R5).
10
12
  // ---------------------------------------------------------------------------
@@ -102,6 +104,30 @@ export function clampTranscriptWindow(v, fallback) {
102
104
  const n = coercePositiveInt(v, fallback);
103
105
  return Math.min(MAX_TRANSCRIPT_WINDOW, Math.max(MIN_TRANSCRIPT_WINDOW, n));
104
106
  }
107
+ // ---------------------------------------------------------------------------
108
+ // Plan mode (plan-mode §4.4)
109
+ //
110
+ // THREE FLAT KEYS, not a nested `planMode` object. `store.ts` documents that its
111
+ // nested-object merge is hand-written and capped at one level; three scalars do
112
+ // not justify a second `clampXConfig` gate plus two more merge sites, and flat
113
+ // keys mean `store.ts` needs ZERO changes.
114
+ // ---------------------------------------------------------------------------
115
+ /** `ask_user` calls allowed per user turn. */
116
+ export const DEFAULT_PLAN_MAX_ASK_ROUNDS = 4;
117
+ const ASK_ROUNDS_RANGE = { min: 1, max: 10 };
118
+ /**
119
+ * Ceiling on ONE human wait, in ms. Thirty minutes is far outside "the user
120
+ * went to get coffee", but a ceiling has to exist: an unattended terminal must
121
+ * not wedge a CI job or a stale ssh session forever.
122
+ */
123
+ export const DEFAULT_PLAN_HUMAN_TIMEOUT_MS = 1_800_000;
124
+ const HUMAN_TIMEOUT_RANGE = { min: 60_000, max: 7_200_000 };
125
+ export function clampAskRounds(v, fallback) {
126
+ return clampInt(v, fallback, ASK_ROUNDS_RANGE);
127
+ }
128
+ export function clampHumanTimeout(v, fallback) {
129
+ return clampInt(v, fallback, HUMAN_TIMEOUT_RANGE);
130
+ }
105
131
  export const SKILLS_INTEGRITY_MODES = ['off', 'warn', 'strict'];
106
132
  export function clampSkillsIntegrity(v, fallback) {
107
133
  return typeof v === 'string' && SKILLS_INTEGRITY_MODES.includes(v)
@@ -181,6 +207,59 @@ export function clampSkillsConfig(raw) {
181
207
  };
182
208
  }
183
209
  export const DEFAULT_SKILLS_RUNTIME = { forcedSkills: [], approveAll: false };
210
+ const DEFAULT_MAX_LOG_FILE_BYTES = 5 * 1024 * 1024;
211
+ const LOG_FILE_BYTES_RANGE = { min: 64 * 1024, max: 256 * 1024 * 1024 };
212
+ const LOG_MAX_FILES_RANGE = { min: 1, max: 200 };
213
+ const LOG_PREVIEW_CHARS_RANGE = { min: 0, max: 8192 };
214
+ export const DEFAULT_LOG_CONFIG = {
215
+ level: 'info',
216
+ toFile: true,
217
+ dir: '',
218
+ maxFileBytes: DEFAULT_MAX_LOG_FILE_BYTES,
219
+ maxFiles: 10,
220
+ redactSecrets: true,
221
+ previewChars: 512,
222
+ };
223
+ /**
224
+ * Coerce a log directory. Anything relative is discarded (falling back to
225
+ * `<home>/logs`) rather than resolved: a relative path would silently mean a
226
+ * different directory for every working directory the CLI is launched from, and
227
+ * `debug`-level files carry prompt text (R-13).
228
+ */
229
+ function logDir(v, fallback) {
230
+ if (typeof v !== 'string')
231
+ return fallback;
232
+ const trimmed = v.trim();
233
+ if (trimmed.length === 0)
234
+ return '';
235
+ return isAbsolute(trimmed) ? trimmed : fallback;
236
+ }
237
+ /**
238
+ * THE single gate for every log-config read AND write, exactly like
239
+ * `clampSkillsConfig`: hardening only the read path leaves a bad value on disk
240
+ * that reverts on every launch, which presents as "my setting won't stick".
241
+ *
242
+ * `previewChars` uses its own coercion because `coercePositiveInt` rejects `0`,
243
+ * and `0` is a legitimate value here — it means "record that content existed,
244
+ * but none of it".
245
+ */
246
+ export function clampLogConfig(raw) {
247
+ const src = (raw && typeof raw === 'object' ? raw : {});
248
+ const preview = typeof src.previewChars === 'number' || typeof src.previewChars === 'string'
249
+ ? Number.parseInt(String(src.previewChars), 10)
250
+ : Number.NaN;
251
+ return {
252
+ level: clampLogLevel(src.level, DEFAULT_LOG_CONFIG.level),
253
+ toFile: bool(src.toFile, DEFAULT_LOG_CONFIG.toFile),
254
+ dir: logDir(src.dir, DEFAULT_LOG_CONFIG.dir),
255
+ maxFileBytes: clampInt(src.maxFileBytes, DEFAULT_LOG_CONFIG.maxFileBytes, LOG_FILE_BYTES_RANGE),
256
+ maxFiles: clampInt(src.maxFiles, DEFAULT_LOG_CONFIG.maxFiles, LOG_MAX_FILES_RANGE),
257
+ redactSecrets: bool(src.redactSecrets, DEFAULT_LOG_CONFIG.redactSecrets),
258
+ previewChars: Number.isFinite(preview)
259
+ ? Math.min(LOG_PREVIEW_CHARS_RANGE.max, Math.max(LOG_PREVIEW_CHARS_RANGE.min, preview))
260
+ : DEFAULT_LOG_CONFIG.previewChars,
261
+ };
262
+ }
184
263
  export const DEFAULT_CONFIG = {
185
264
  version: CONFIG_VERSION,
186
265
  provider: 'anthropic',
@@ -201,8 +280,14 @@ export const DEFAULT_CONFIG = {
201
280
  promptHistory: [],
202
281
  density: 'comfortable',
203
282
  hints: true,
283
+ mouse: true,
284
+ mouseNoticeSeen: false,
204
285
  submitCount: 0,
286
+ planModeDefault: false,
287
+ planModeMaxAskRounds: DEFAULT_PLAN_MAX_ASK_ROUNDS,
288
+ planModeHumanTimeoutMs: DEFAULT_PLAN_HUMAN_TIMEOUT_MS,
205
289
  skills: DEFAULT_SKILLS_CONFIG,
290
+ log: DEFAULT_LOG_CONFIG,
206
291
  };
207
292
  // ---------------------------------------------------------------------------
208
293
  // Coercion / validation helpers
@@ -1,19 +1,35 @@
1
1
  /**
2
- * Config store — read/atomic-write the user config JSON and resolve OS-specific
3
- * paths via `env-paths`. The file may hold secrets, so it is written with
4
- * `0600` permissions on POSIX (spec §3.5 / §6.1).
2
+ * Config store — read/atomic-write the user config JSON. Paths come from
3
+ * `app-paths.ts`; nothing here does its own path arithmetic. The file may hold
4
+ * secrets, so it is written with `0600` permissions on POSIX (spec §3.5 / §6.1).
5
+ *
6
+ * There is deliberately no `getConfigDir()` / `getSkillsDir()` here any more.
7
+ * The first would now return the entire user root under a name that says
8
+ * otherwise (see the test-isolation contract in `app-paths.ts`); the second was
9
+ * a second name for `skills/paths.ts::getUserSkillsDir()`, which is precisely
10
+ * the drift the header of that module warns about.
5
11
  */
12
+ import { getConfigPath, getSessionsDir } from './app-paths.js';
6
13
  import { type PersistedConfig } from './schema.js';
7
- export declare function getConfigDir(): string;
8
- export declare function getConfigPath(): string;
9
- export declare function getSessionsDir(): string;
10
- /** User-scope skills root — `aragon skills install`'s default target. */
11
- export declare function getSkillsDir(): string;
14
+ export { getConfigPath, getSessionsDir };
15
+ export interface ConfigFileRead {
16
+ /** Only the keys the user actually wrote, or `null` when absent/unusable. */
17
+ config: Partial<PersistedConfig> | null;
18
+ /** Set when the file EXISTS but could not be parsed. Otherwise undefined. */
19
+ parseError?: string;
20
+ }
12
21
  /**
13
- * Read the persisted config file. Returns a partial config (only the keys the
14
- * user actually wrote) or `null` when the file is absent or unparseable.
22
+ * Read the persisted config file.
23
+ *
24
+ * A corrupt config file still must never crash the CLI — the fallback to
25
+ * defaults is unchanged. What changed is that it is no longer SILENT: a single
26
+ * stray comma used to reset the model, theme, timeouts, skills and (as far as
27
+ * the user could tell) the API key, with nothing anywhere saying why. Now the
28
+ * failure travels out of band in `parseError`, and `loadConfig()` turns it into
29
+ * a log record plus one visible line. Hand-editing this file is a supported
30
+ * workflow, so the failure mode had to become visible.
15
31
  */
16
- export declare function readConfigFile(): Partial<PersistedConfig> | null;
32
+ export declare function readConfigFile(): ConfigFileRead;
17
33
  /**
18
34
  * Atomically write the persisted config: write a temp file in the same
19
35
  * directory, then rename over the target, then re-apply `0600` on POSIX.
@@ -39,6 +55,8 @@ export declare function loadPersistedConfig(): PersistedConfig;
39
55
  *
40
56
  * `SkillsConfig` is therefore capped at ONE level of nesting (scalars and
41
57
  * string arrays only). A third level requires replacing this hand-written merge
42
- * with a real deep-merge utility first, or the same bug returns.
58
+ * with a real deep-merge utility first, or the same bug returns. `LogConfig` is
59
+ * a third SECTION, but it is still one level deep (scalars only), so the same
60
+ * hand-written merge covers it — and it must stay that way for that reason.
43
61
  */
44
62
  export declare function updatePersistedConfig(patch: Partial<PersistedConfig>): PersistedConfig;