@aragon-agent/cli 0.6.11 → 0.6.13
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.
- package/CHANGELOG.md +10 -1
- package/README.md +97 -20
- package/dist/agent/controller.d.ts +38 -3
- package/dist/agent/controller.js +151 -60
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +199 -0
- package/dist/agent/queued-messages.d.ts +20 -0
- package/dist/agent/queued-messages.js +46 -0
- package/dist/agent/reducer.d.ts +21 -2
- package/dist/agent/reducer.js +65 -37
- package/dist/cli.js +15 -1
- package/dist/commands/builtins.js +83 -42
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +4 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +13 -11
- package/dist/compaction/context-command.js +5 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +6 -1
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +2 -1
- package/dist/config/load.js +15 -8
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/enter-sequences.d.ts +11 -10
- package/dist/input/enter-sequences.js +29 -27
- package/dist/input/stdin-filter.js +93 -28
- package/dist/logging/install.d.ts +3 -0
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/session/persist.d.ts +6 -7
- package/dist/session/persist.js +6 -9
- package/dist/session/store.d.ts +4 -1
- package/dist/session/validate-session.d.ts +3 -0
- package/dist/session/validate-session.js +207 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +161 -226
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +18 -84
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/PromptInput.d.ts +6 -2
- package/dist/ui/PromptInput.js +91 -105
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +24 -0
- package/dist/ui/QueueStatusRow.js +51 -0
- package/dist/ui/StatusBar.d.ts +15 -149
- package/dist/ui/StatusBar.js +27 -112
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/composer-input.d.ts +28 -0
- package/dist/ui/composer-input.js +46 -0
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +9 -0
- package/dist/ui/editor-reducer.js +31 -15
- package/dist/ui/enter-frames.d.ts +15 -31
- package/dist/ui/enter-frames.js +14 -33
- package/dist/ui/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +109 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +11 -3
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +58 -20
- package/dist/ui/transcript-text.js +20 -12
- package/dist/ui/use-wheel-routing.js +2 -1
- package/package.json +2 -2
package/dist/fast/resolve.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* at construction is a tier that can be describing a provider the session no
|
|
19
19
|
* longer uses.
|
|
20
20
|
*/
|
|
21
|
+
import type { ModelRole } from '../config/model-profiles.js';
|
|
21
22
|
import { type CliConfig } from '../config/schema.js';
|
|
22
23
|
import type { FastTier } from './types.js';
|
|
23
24
|
/**
|
|
@@ -29,7 +30,7 @@ import type { FastTier } from './types.js';
|
|
|
29
30
|
* `--api-key` override. Injected rather than reimplemented, because a second
|
|
30
31
|
* key resolver is a second answer to "why does it say no key?".
|
|
31
32
|
*/
|
|
32
|
-
export declare function resolveFastTier(config: CliConfig, hasKey: (providerId: string) => boolean): FastTier;
|
|
33
|
+
export declare function resolveFastTier(config: CliConfig, hasKey: (providerId: string, role?: ModelRole) => boolean): FastTier;
|
|
33
34
|
/**
|
|
34
35
|
* The one-line notice for a tier the user switched on that cannot resolve, or
|
|
35
36
|
* `null` when there is nothing worth saying.
|
package/dist/fast/resolve.js
CHANGED
|
@@ -47,7 +47,7 @@ export function resolveFastTier(config, hasKey) {
|
|
|
47
47
|
// different settings.
|
|
48
48
|
if (!isAdapterProvider(provider))
|
|
49
49
|
return { ok: false, reason: 'no_adapter' };
|
|
50
|
-
if (!hasKey(provider))
|
|
50
|
+
if (!hasKey(provider, 'fast'))
|
|
51
51
|
return { ok: false, reason: 'no_key' };
|
|
52
52
|
// 6. A fast tier on the SAME provider inherits the session's base URL: a user
|
|
53
53
|
// pointing the CLI at a gateway means both models, not one. A fast tier on
|
|
@@ -56,9 +56,11 @@ export function resolveFastTier(config, hasKey) {
|
|
|
56
56
|
const explicitBaseUrl = fast.baseUrl.trim();
|
|
57
57
|
const baseUrl = explicitBaseUrl.length > 0
|
|
58
58
|
? explicitBaseUrl
|
|
59
|
-
:
|
|
60
|
-
?
|
|
61
|
-
:
|
|
59
|
+
: config.modelProfiles?.fastId && !config.modelProfileState?.fast.invalid
|
|
60
|
+
? undefined
|
|
61
|
+
: provider === config.provider
|
|
62
|
+
? config.baseUrl
|
|
63
|
+
: undefined;
|
|
62
64
|
const ref = {
|
|
63
65
|
providerId: provider,
|
|
64
66
|
modelId,
|
package/dist/fast/wiring.d.ts
CHANGED
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
* inside its own loop with an error naming neither the tier nor the setting that
|
|
31
31
|
* caused it (`agent-loop.ts:140-145`).
|
|
32
32
|
*/
|
|
33
|
+
import type { ModelRole } from '../config/model-profiles.js';
|
|
33
34
|
import { type AgentEvent, type AssistantMessage, type LLMRequest, type ModelRef, type ThinkingLevel } from '@aragon-agent/core';
|
|
34
35
|
import type { CliConfig } from '../config/schema.js';
|
|
35
36
|
import type { NoticeLevel } from '../agent/reducer.js';
|
|
@@ -38,8 +39,8 @@ export interface FastWiringDeps {
|
|
|
38
39
|
/** Read LIVE on every call - the controller replaces the object on mutation. */
|
|
39
40
|
getConfig: () => CliConfig;
|
|
40
41
|
/** `AgentController.hasApiKey`, so there is exactly one key resolver. */
|
|
41
|
-
hasKey: (providerId: string) => boolean;
|
|
42
|
-
getApiKey: (providerId: string) => string | undefined;
|
|
42
|
+
hasKey: (providerId: string, role?: ModelRole) => boolean;
|
|
43
|
+
getApiKey: (providerId: string, role?: ModelRole) => string | undefined;
|
|
43
44
|
/** Whether the static price table knows this model (C-11 / RV-4). */
|
|
44
45
|
isPricedModel: (ref: Pick<ModelRef, 'providerId' | 'modelId'>) => boolean;
|
|
45
46
|
/** The lead agent's event stream and steering surface. */
|
|
@@ -145,7 +146,7 @@ export declare class FastWiring {
|
|
|
145
146
|
* resolved model id and a prompt naming a model the tier will not use is worse
|
|
146
147
|
* than no prompt at all.
|
|
147
148
|
*/
|
|
148
|
-
onConfigChanged(): void;
|
|
149
|
+
onConfigChanged(refreshPrompt?: boolean, enabled?: boolean): void;
|
|
149
150
|
/**
|
|
150
151
|
* The startup notice for a configuration mistake that is otherwise invisible
|
|
151
152
|
* (§3.3). Raised once, at construction, through the controller's `notify`.
|
|
@@ -162,6 +163,7 @@ export declare class FastWiring {
|
|
|
162
163
|
resolveTier(tier: FastTierName): {
|
|
163
164
|
ref: ModelRef;
|
|
164
165
|
thinkingLevel: ThinkingLevel;
|
|
166
|
+
role?: ModelRole;
|
|
165
167
|
};
|
|
166
168
|
/** The resolved fast `ModelRef`, or `null` - for per-tier cost lookup (§3.6). */
|
|
167
169
|
fastRef(): ModelRef | null;
|
package/dist/fast/wiring.js
CHANGED
|
@@ -114,7 +114,7 @@ export class FastWiring {
|
|
|
114
114
|
getTier: () => this.tier,
|
|
115
115
|
getConfig: () => this.deps.getConfig().fast,
|
|
116
116
|
available: () => this.available(),
|
|
117
|
-
getApiKey: deps.getApiKey,
|
|
117
|
+
getApiKey: (id) => deps.getApiKey(id, 'fast'),
|
|
118
118
|
emit: (event) => this.emit(event),
|
|
119
119
|
// The reviewer announces its own self-disable with a `tier_changed`, and
|
|
120
120
|
// only the wiring can build the snapshot that carries it.
|
|
@@ -202,7 +202,9 @@ export class FastWiring {
|
|
|
202
202
|
* resolved model id and a prompt naming a model the tier will not use is worse
|
|
203
203
|
* than no prompt at all.
|
|
204
204
|
*/
|
|
205
|
-
onConfigChanged() {
|
|
205
|
+
onConfigChanged(refreshPrompt = true, enabled) {
|
|
206
|
+
if (enabled !== undefined)
|
|
207
|
+
this.enabled = enabled;
|
|
206
208
|
const next = resolveFastTier(this.deps.getConfig(), this.deps.hasKey);
|
|
207
209
|
const before = this.tier;
|
|
208
210
|
this.tier = next;
|
|
@@ -219,7 +221,8 @@ export class FastWiring {
|
|
|
219
221
|
this.deps.notify('warn', text);
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
|
-
|
|
224
|
+
if (refreshPrompt)
|
|
225
|
+
this.deps.onPromptChanged();
|
|
223
226
|
this.emit({ type: 'tier_changed', snapshot: this.snapshot() });
|
|
224
227
|
}
|
|
225
228
|
/**
|
|
@@ -243,7 +246,7 @@ export class FastWiring {
|
|
|
243
246
|
resolveTier(tier) {
|
|
244
247
|
const config = this.deps.getConfig();
|
|
245
248
|
if (tier === 'fast' && this.delegationAvailable() && this.tier.ok) {
|
|
246
|
-
return { ref: this.tier.ref, thinkingLevel: this.tier.thinkingLevel };
|
|
249
|
+
return { ref: this.tier.ref, thinkingLevel: this.tier.thinkingLevel, role: 'fast' };
|
|
247
250
|
}
|
|
248
251
|
return {
|
|
249
252
|
ref: {
|
|
@@ -252,6 +255,7 @@ export class FastWiring {
|
|
|
252
255
|
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
253
256
|
},
|
|
254
257
|
thinkingLevel: config.thinkingLevel,
|
|
258
|
+
role: 'main',
|
|
255
259
|
};
|
|
256
260
|
}
|
|
257
261
|
/** The resolved fast `ModelRef`, or `null` - for per-tier cost lookup (§3.6). */
|
|
@@ -38,16 +38,17 @@ export interface EnterSequenceSpec {
|
|
|
38
38
|
* `'\r'` rather than to a newline.
|
|
39
39
|
*/
|
|
40
40
|
export declare const ENTER_SEQUENCES: readonly EnterSequenceSpec[];
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
41
|
+
export type EnterSequenceSegment = {
|
|
42
|
+
kind: 'text';
|
|
43
|
+
text: string;
|
|
44
|
+
} | {
|
|
45
|
+
kind: 'newline';
|
|
46
|
+
} | {
|
|
47
|
+
kind: 'submit';
|
|
48
|
+
};
|
|
49
|
+
/** Split explicit keys using the recognition table; bare CR/LF remain text. */
|
|
50
|
+
export declare function splitEnterSequences(text: string): EnterSequenceSegment[];
|
|
51
|
+
/** Normalize explicit Enter keys without changing unknown sequences. */
|
|
51
52
|
export declare function rewriteEnterSequences(text: string): string;
|
|
52
53
|
/**
|
|
53
54
|
* Length of the trailing run of `text` that is a proper prefix of an
|
|
@@ -30,6 +30,7 @@ import { ENTER_NEWLINE_FRAME } from './limits.js';
|
|
|
30
30
|
* `'\r'` rather than to a newline.
|
|
31
31
|
*/
|
|
32
32
|
export const ENTER_SEQUENCES = [
|
|
33
|
+
{ seq: '\x1b[13;1u', meaning: 'enter', to: '\r' },
|
|
33
34
|
{
|
|
34
35
|
seq: '\x1b[13;2u',
|
|
35
36
|
meaning: 'shift+enter',
|
|
@@ -88,34 +89,35 @@ export const ENTER_SEQUENCES = [
|
|
|
88
89
|
* up to 6 of them.
|
|
89
90
|
*/
|
|
90
91
|
const MAX_ENTER_PREFIX = Math.max(...ENTER_SEQUENCES.map((s) => s.seq.length)) - 1;
|
|
91
|
-
/**
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
92
|
+
/** Split explicit keys using the recognition table; bare CR/LF remain text. */
|
|
93
|
+
export function splitEnterSequences(text) {
|
|
94
|
+
const segments = [];
|
|
95
|
+
let start = 0;
|
|
96
|
+
let index = 0;
|
|
97
|
+
while (index < text.length) {
|
|
98
|
+
const match = text[index] === '\x1b'
|
|
99
|
+
? ENTER_SEQUENCES.find((spec) => text.startsWith(spec.seq, index)) : undefined;
|
|
100
|
+
if (!match) {
|
|
101
|
+
index += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (index > start)
|
|
105
|
+
segments.push({ kind: 'text', text: text.slice(start, index) });
|
|
106
|
+
segments.push({ kind: match.to === '\r' ? 'submit' : 'newline' });
|
|
107
|
+
index += match.seq.length;
|
|
108
|
+
start = index;
|
|
109
|
+
}
|
|
110
|
+
if (start < text.length)
|
|
111
|
+
segments.push({ kind: 'text', text: text.slice(start) });
|
|
112
|
+
return segments;
|
|
113
|
+
}
|
|
114
|
+
/** Normalize explicit Enter keys without changing unknown sequences. */
|
|
103
115
|
export function rewriteEnterSequences(text) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
// above records in each entry's `to` field.
|
|
110
|
-
let out = text.replace(CSI_U_ENTER, (seq) => (seq.includes(';') ? ENTER_NEWLINE_FRAME : '\r'));
|
|
111
|
-
// Only the two literal Alt+Enter encodings remain. `\x1b\n` is checked
|
|
112
|
-
// before `\x1b\r` is replaced; they share no bytes, so order is free, but
|
|
113
|
-
// splitting the escapes keeps each replacement self-evident.
|
|
114
|
-
if (out.includes('\x1b\n'))
|
|
115
|
-
out = out.split('\x1b\n').join(ENTER_NEWLINE_FRAME);
|
|
116
|
-
if (out.includes('\x1b\r'))
|
|
117
|
-
out = out.split('\x1b\r').join(ENTER_NEWLINE_FRAME);
|
|
118
|
-
return out;
|
|
116
|
+
return splitEnterSequences(text).map((segment) => {
|
|
117
|
+
if (segment.kind === 'text')
|
|
118
|
+
return segment.text;
|
|
119
|
+
return segment.kind === 'submit' ? '\r' : ENTER_NEWLINE_FRAME;
|
|
120
|
+
}).join('');
|
|
119
121
|
}
|
|
120
122
|
/** True when `s` is a PROPER prefix of some recognized sequence (never whole). */
|
|
121
123
|
function isEnterPrefix(s) {
|
|
@@ -33,9 +33,9 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { PassThrough } from 'node:stream';
|
|
35
35
|
import { MAX_PENDING_MOUSE_CHARS, splitMouseEvents, isMousePrefix, } from './mouse-events.js';
|
|
36
|
-
import { PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
|
|
36
|
+
import { ENTER_NEWLINE_FRAME, PASTE_ASSEMBLY_MAX_MS, PASTE_BURST_MS, PASTE_CLOSE, PASTE_MAX_BYTES, PASTE_OPEN, formatPasteSize, } from './limits.js';
|
|
37
37
|
import { PASTE_BEGIN_MARK, PASTE_END_MARK, classifyChunk, sanitisePaste, trailingPastePrefixLength, } from './paste-parse.js';
|
|
38
|
-
import {
|
|
38
|
+
import { splitEnterSequences, trailingEnterPrefixLength } from './enter-sequences.js';
|
|
39
39
|
/**
|
|
40
40
|
* How long a possible-prefix tail is held before being flushed as text.
|
|
41
41
|
*
|
|
@@ -89,7 +89,18 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
89
89
|
let burstTimer = null;
|
|
90
90
|
let assemblyTimer = null;
|
|
91
91
|
let disposed = false;
|
|
92
|
+
let finished = false;
|
|
92
93
|
let paste = { kind: 'idle' };
|
|
94
|
+
let discardBracketed = false;
|
|
95
|
+
let outputBatch = null;
|
|
96
|
+
const writeInput = (text) => {
|
|
97
|
+
if (disposed || outputBatch?.refused)
|
|
98
|
+
return;
|
|
99
|
+
if (outputBatch)
|
|
100
|
+
outputBatch.chunks.push(text);
|
|
101
|
+
else
|
|
102
|
+
wrapper.write(text);
|
|
103
|
+
};
|
|
93
104
|
// --- The surface Ink reads off the stream it is handed. ------------------
|
|
94
105
|
// `isRawModeSupported()` reads `props.stdin.isTTY` (App.js:34), so this has
|
|
95
106
|
// to be a DELEGATING GETTER rather than a snapshot: a stdin that is not a TTY
|
|
@@ -122,6 +133,8 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
122
133
|
};
|
|
123
134
|
/** `.unref()` so a held tail can never keep the process alive on its own. */
|
|
124
135
|
const arm = (fn, ms) => {
|
|
136
|
+
if (finished || disposed)
|
|
137
|
+
return null;
|
|
125
138
|
const t = setTimeout(fn, ms);
|
|
126
139
|
t.unref?.();
|
|
127
140
|
return t;
|
|
@@ -143,6 +156,10 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
143
156
|
if (disposed)
|
|
144
157
|
return;
|
|
145
158
|
if (overflow || bytes > PASTE_MAX_BYTES) {
|
|
159
|
+
if (outputBatch?.refused)
|
|
160
|
+
return;
|
|
161
|
+
if (outputBatch)
|
|
162
|
+
outputBatch.refused = true;
|
|
146
163
|
const text = `Paste too large (${formatPasteSize(bytes)}); limit is ` +
|
|
147
164
|
`${formatPasteSize(PASTE_MAX_BYTES)}. Nothing was inserted.`;
|
|
148
165
|
onWarn?.(text);
|
|
@@ -153,7 +170,7 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
153
170
|
if (payload.length === 0)
|
|
154
171
|
return;
|
|
155
172
|
// ONE `write()` call, at the position the paste occupied in the stream.
|
|
156
|
-
|
|
173
|
+
writeInput(PASTE_OPEN + payload + PASTE_CLOSE);
|
|
157
174
|
};
|
|
158
175
|
const flushBurst = () => {
|
|
159
176
|
burstTimer = clearTimer(burstTimer);
|
|
@@ -169,6 +186,12 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
169
186
|
return;
|
|
170
187
|
const { body, bytes, overflow } = paste;
|
|
171
188
|
paste = { kind: 'idle' };
|
|
189
|
+
if (discardBracketed) {
|
|
190
|
+
discardBracketed = false;
|
|
191
|
+
if (outputBatch)
|
|
192
|
+
outputBatch.refused = true;
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
172
195
|
if (reason === 'timeout') {
|
|
173
196
|
// I-4: an unterminated marker must never wedge input. Emitting what
|
|
174
197
|
// arrived is strictly better than holding the keyboard hostage.
|
|
@@ -190,20 +213,9 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
190
213
|
* those would corrupt their data silently. v1 covered only Tier 1, which left
|
|
191
214
|
* the loss in place for exactly the terminals Tier 2 exists to serve (P1-7).
|
|
192
215
|
*/
|
|
193
|
-
const
|
|
216
|
+
const handleText = (text, holdTail) => {
|
|
194
217
|
if (text.length === 0)
|
|
195
218
|
return;
|
|
196
|
-
// ENTER-FAMILY REWRITE, AND IT IS FIRST FOR A REASON
|
|
197
|
-
// (tui-shift-enter-copy-queue 3.4). The Alt+Enter encoding contains a CR,
|
|
198
|
-
// and `classifyChunk`'s primary rule -- "longer than one character AND
|
|
199
|
-
// contains a line break" -- would buffer that two-byte key as a Tier 2
|
|
200
|
-
// paste body whose sanitiser then strips the ESC, silently swallowing the
|
|
201
|
-
// keystroke. After the rewrite the frame is NUL-led and classifies as
|
|
202
|
-
// `keys` under both rules. The cost is that an UNBRACKETED paste whose
|
|
203
|
-
// body happens to contain the literal Alt+Enter bytes loses that one
|
|
204
|
-
// pair's newline; a bracketed body never reaches this function at all
|
|
205
|
-
// (I-3), and losing two bytes of a heuristic tier beats eating the key.
|
|
206
|
-
text = rewriteEnterSequences(text);
|
|
207
219
|
if (paste.kind === 'burst') {
|
|
208
220
|
paste = { kind: 'burst', body: paste.body + text, bytes: paste.bytes + byteLength(text) };
|
|
209
221
|
burstTimer = clearTimer(burstTimer);
|
|
@@ -216,15 +228,16 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
216
228
|
burstTimer = arm(flushBurst, PASTE_BURST_MS);
|
|
217
229
|
return;
|
|
218
230
|
}
|
|
219
|
-
//
|
|
220
|
-
//
|
|
231
|
+
// Ink conflates DEL Backspace with CSI Delete. Normalize only keyboard
|
|
232
|
+
// text, after burst classification, preserving ESC for Alt+Backspace.
|
|
233
|
+
text = text.replace(/\x7f/g, '\x08');
|
|
221
234
|
if (!features.mouse) {
|
|
222
|
-
|
|
235
|
+
writeInput(text);
|
|
223
236
|
return;
|
|
224
237
|
}
|
|
225
238
|
const split = splitMouseEvents(text);
|
|
226
239
|
if (split.text.length > 0)
|
|
227
|
-
|
|
240
|
+
writeInput(split.text);
|
|
228
241
|
for (const event of split.events)
|
|
229
242
|
emit(event);
|
|
230
243
|
if (split.pending.length === 0)
|
|
@@ -236,10 +249,23 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
236
249
|
if (holdTail)
|
|
237
250
|
pending += split.pending;
|
|
238
251
|
else
|
|
239
|
-
|
|
252
|
+
writeInput(split.pending);
|
|
253
|
+
};
|
|
254
|
+
const handleOutsidePaste = (text, holdTail) => {
|
|
255
|
+
const segments = splitEnterSequences(text);
|
|
256
|
+
for (let index = 0; index < segments.length; index += 1) {
|
|
257
|
+
const segment = segments[index];
|
|
258
|
+
if (segment.kind === 'text') {
|
|
259
|
+
handleText(segment.text, holdTail && index === segments.length - 1);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
// A key terminates the heuristic paste before its internal frame is written.
|
|
263
|
+
flushBurst();
|
|
264
|
+
writeInput(segment.kind === 'submit' ? '\r' : ENTER_NEWLINE_FRAME);
|
|
265
|
+
}
|
|
240
266
|
};
|
|
241
267
|
const appendBracketed = (text) => {
|
|
242
|
-
if (paste.kind !== 'bracketed' || text.length === 0)
|
|
268
|
+
if (paste.kind !== 'bracketed' || text.length === 0 || discardBracketed)
|
|
243
269
|
return;
|
|
244
270
|
const bytes = paste.bytes + byteLength(text);
|
|
245
271
|
const overflow = paste.overflow || bytes > PASTE_MAX_BYTES;
|
|
@@ -315,13 +341,39 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
315
341
|
handleOutsidePaste(text, false);
|
|
316
342
|
};
|
|
317
343
|
const onData = (chunk) => {
|
|
318
|
-
if (disposed)
|
|
344
|
+
if (disposed || finished)
|
|
319
345
|
return;
|
|
320
346
|
flushTimer = clearTimer(flushTimer);
|
|
321
347
|
const decoded = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
322
348
|
const carried = pending;
|
|
323
349
|
pending = '';
|
|
324
|
-
|
|
350
|
+
// A rejected paste must not leave earlier text or a later Enter executable.
|
|
351
|
+
const batch = { chunks: [], refused: false };
|
|
352
|
+
outputBatch = batch;
|
|
353
|
+
try {
|
|
354
|
+
feed(carried + decoded.replace(/\u0000/g, ''));
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
outputBatch = null;
|
|
358
|
+
}
|
|
359
|
+
if (batch.refused) {
|
|
360
|
+
burstTimer = clearTimer(burstTimer);
|
|
361
|
+
if (paste.kind === 'bracketed') {
|
|
362
|
+
// Keep consuming an unfinished body so its next chunk cannot become keys.
|
|
363
|
+
discardBracketed = true;
|
|
364
|
+
paste = { kind: 'bracketed', body: '', bytes: 0, overflow: false };
|
|
365
|
+
}
|
|
366
|
+
else {
|
|
367
|
+
pending = '';
|
|
368
|
+
paste = { kind: 'idle' };
|
|
369
|
+
assemblyTimer = clearTimer(assemblyTimer);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (disposed || finished)
|
|
374
|
+
return;
|
|
375
|
+
for (const text of batch.chunks)
|
|
376
|
+
wrapper.write(text);
|
|
325
377
|
if (paste.kind === 'bracketed' || pending.length === 0)
|
|
326
378
|
return;
|
|
327
379
|
if (pending.length > MAX_PENDING_CHARS) {
|
|
@@ -337,11 +389,24 @@ export function createStdinFilter(real, features, bridge, onWarn) {
|
|
|
337
389
|
// P2-7: a closed stdin must still terminate Ink's reader, or it waits forever
|
|
338
390
|
// on a stream that will never produce another byte.
|
|
339
391
|
const onEnd = () => {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
392
|
+
if (finished || disposed)
|
|
393
|
+
return;
|
|
394
|
+
finished = true;
|
|
395
|
+
flushTimer = clearTimer(flushTimer);
|
|
396
|
+
burstTimer = clearTimer(burstTimer);
|
|
397
|
+
assemblyTimer = clearTimer(assemblyTimer);
|
|
398
|
+
const tail = pending;
|
|
399
|
+
pending = '';
|
|
400
|
+
if (paste.kind === 'bracketed') {
|
|
401
|
+
appendBracketed(tail);
|
|
402
|
+
flushBracketed('timeout');
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
// The pending prefix belongs to the currently open burst, if any.
|
|
406
|
+
handleOutsidePaste(tail, false);
|
|
407
|
+
flushBurst();
|
|
408
|
+
}
|
|
409
|
+
wrapper.end();
|
|
345
410
|
};
|
|
346
411
|
// `setEncoding('utf8')` on the REAL stream so Node's StringDecoder handles
|
|
347
412
|
// multi-byte boundaries for us -- the scan works on characters, and a UTF-8
|
package/dist/logging/install.js
CHANGED
|
@@ -42,7 +42,7 @@ import { readConfigFile } from '../config/store.js';
|
|
|
42
42
|
import { clampLogConfig } from '../config/schema.js';
|
|
43
43
|
import { clampLogLevel } from './levels.js';
|
|
44
44
|
import { Logger, setActiveLogger } from './logger.js';
|
|
45
|
-
import { registerSecretsFrom } from './secret-registry.js';
|
|
45
|
+
import { registerProfileSecrets, registerSecretsFrom } from './secret-registry.js';
|
|
46
46
|
/** POSIX signal numbers for the `128 + signo` exit-code convention. */
|
|
47
47
|
const SIGNAL_NUMBERS = { SIGINT: 2, SIGTERM: 15, SIGHUP: 1 };
|
|
48
48
|
let screenRestore = () => { };
|
|
@@ -213,7 +213,9 @@ export function installLogging(opts = {}) {
|
|
|
213
213
|
// Registration site 1 (§4.4.3): whatever the config file already holds. Sites
|
|
214
214
|
// 2, 3, 4 and 5 add the env, the merged view plus `--api-key`, the settings
|
|
215
215
|
// screen, and every persisted write respectively.
|
|
216
|
-
|
|
216
|
+
const persisted = readConfigFile().config;
|
|
217
|
+
registerSecretsFrom(persisted?.apiKeys ?? undefined);
|
|
218
|
+
registerProfileSecrets(persisted);
|
|
217
219
|
const warning = getHomeResolutionWarning();
|
|
218
220
|
if (warning)
|
|
219
221
|
logger.warn('cli', 'home_resolution_fallback', { reason: warning });
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
export declare function registerSecret(value: string | undefined | null): void;
|
|
19
19
|
/** Convenience for the `apiKeys` maps every registration site happens to hold. */
|
|
20
20
|
export declare function registerSecretsFrom(keys: Record<string, string | null | undefined> | undefined): void;
|
|
21
|
+
/** Register dedicated credentials in untrusted parsed config before schema validation. */
|
|
22
|
+
export declare function registerProfileSecrets(value: unknown): void;
|
|
21
23
|
/**
|
|
22
24
|
* Every known secret, LONGEST FIRST.
|
|
23
25
|
*
|
|
@@ -39,6 +39,24 @@ export function registerSecretsFrom(keys) {
|
|
|
39
39
|
for (const value of Object.values(keys))
|
|
40
40
|
registerSecret(value);
|
|
41
41
|
}
|
|
42
|
+
/** Register dedicated credentials in untrusted parsed config before schema validation. */
|
|
43
|
+
export function registerProfileSecrets(value) {
|
|
44
|
+
if (!value || typeof value !== 'object')
|
|
45
|
+
return;
|
|
46
|
+
const profiles = value.modelProfiles;
|
|
47
|
+
if (!profiles || typeof profiles !== 'object')
|
|
48
|
+
return;
|
|
49
|
+
const entries = profiles.entries;
|
|
50
|
+
if (!Array.isArray(entries))
|
|
51
|
+
return;
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
if (!entry || typeof entry !== 'object')
|
|
54
|
+
continue;
|
|
55
|
+
const key = entry.apiKey;
|
|
56
|
+
if (typeof key === 'string')
|
|
57
|
+
registerSecret(key);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
42
60
|
/**
|
|
43
61
|
* Every known secret, LONGEST FIRST.
|
|
44
62
|
*
|
|
@@ -39,14 +39,13 @@ export interface SessionMeta {
|
|
|
39
39
|
export interface SavedSession {
|
|
40
40
|
version: number;
|
|
41
41
|
savedAt: number;
|
|
42
|
-
model
|
|
42
|
+
model?: ModelRef;
|
|
43
43
|
messages: Message[];
|
|
44
44
|
entries: Entry[];
|
|
45
45
|
/**
|
|
46
46
|
* The live todo list at save time (todo-plan-execution §3.13).
|
|
47
47
|
*
|
|
48
|
-
* `SESSION_VERSION` STAYS 1: the field is additive and optional,
|
|
49
|
-
* validates only that `messages` and `entries` are arrays, and an older file
|
|
48
|
+
* `SESSION_VERSION` STAYS 1: the field is additive and optional, and an older file
|
|
50
49
|
* simply yields `undefined` — which `restoreTodos` treats as "clear", the
|
|
51
50
|
* right answer for a session that predates the feature (D-20 / D-22).
|
|
52
51
|
*/
|
|
@@ -56,8 +55,8 @@ export interface SavedSession {
|
|
|
56
55
|
*
|
|
57
56
|
* `SESSION_VERSION` STAYS 1 for the reason `todos?` records above, and this is
|
|
58
57
|
* the precedent that file's own comments already set: the field is additive
|
|
59
|
-
* and optional
|
|
60
|
-
*
|
|
58
|
+
* and optional. Boundary validation preserves it without changing its shape.
|
|
59
|
+
* A TUI `/save` file simply has no `meta`, which is exactly how
|
|
61
60
|
* `sessions list` tells the two apart.
|
|
62
61
|
*/
|
|
63
62
|
meta?: SessionMeta;
|
|
@@ -88,8 +87,8 @@ export declare function saveSession(filePath: string, data: {
|
|
|
88
87
|
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5),
|
|
89
88
|
* and any todo card that was saved mid-run (todo-plan-execution §3.8 / C-5).
|
|
90
89
|
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
90
|
+
* Structural validation cannot settle live state: a session saved while a
|
|
91
|
+
* dispatch was in flight can contain a valid
|
|
93
92
|
* `kind: 'team'` entry claiming `active: true` while nothing is running. TWO
|
|
94
93
|
* things then go wrong at once: the card spins forever, and `Transcript`'s
|
|
95
94
|
* settled boundary is MONOTONIC — an entry that never settles never reaches
|
package/dist/session/persist.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname, isAbsolute, join, resolve } from 'node:path';
|
|
10
10
|
import { getSessionsDir } from '../config/store.js';
|
|
11
|
+
import { validateSession } from './validate-session.js';
|
|
11
12
|
const SESSION_VERSION = 1;
|
|
12
13
|
/** Resolve a `/save` target: absolute/relative path, or a name in the data dir. */
|
|
13
14
|
export function resolveSessionPath(nameOrPath, cwd) {
|
|
@@ -109,8 +110,8 @@ export function saveSession(filePath, data) {
|
|
|
109
110
|
* Settle any team entry that was saved mid-dispatch (team-subagents §5.3 / P1-5),
|
|
110
111
|
* and any todo card that was saved mid-run (todo-plan-execution §3.8 / C-5).
|
|
111
112
|
*
|
|
112
|
-
*
|
|
113
|
-
*
|
|
113
|
+
* Structural validation cannot settle live state: a session saved while a
|
|
114
|
+
* dispatch was in flight can contain a valid
|
|
114
115
|
* `kind: 'team'` entry claiming `active: true` while nothing is running. TWO
|
|
115
116
|
* things then go wrong at once: the card spins forever, and `Transcript`'s
|
|
116
117
|
* settled boundary is MONOTONIC — an entry that never settles never reaches
|
|
@@ -196,7 +197,7 @@ export function normalizeLoadedEntries(entries) {
|
|
|
196
197
|
// THE SEVENTH CLAUSE (tui-shift-enter-copy-queue 5.2.5). Core's steering
|
|
197
198
|
// queue is memory-only, so a `queued` entry on disk is a promise this
|
|
198
199
|
// process can no longer keep: the message was never accepted into any
|
|
199
|
-
// conversation. Downgrade to a WARN NOTICE carrying the
|
|
200
|
+
// conversation. Downgrade to a WARN NOTICE carrying the full text
|
|
200
201
|
// rather than to a `user` entry -- pretending it was sent is the lie
|
|
201
202
|
// that loses the user's words with a straight face.
|
|
202
203
|
//
|
|
@@ -204,12 +205,11 @@ export function normalizeLoadedEntries(entries) {
|
|
|
204
205
|
// live transcript, and a fresh id here would collide with the next
|
|
205
206
|
// `e<n>` the reducer allocates.
|
|
206
207
|
if (entry.kind === 'queued') {
|
|
207
|
-
const firstLine = entry.text.split('\n', 1)[0] ?? '';
|
|
208
208
|
const downgraded = {
|
|
209
209
|
id: entry.id,
|
|
210
210
|
kind: 'notice',
|
|
211
211
|
level: 'warn',
|
|
212
|
-
text: `Queued but never sent: ${
|
|
212
|
+
text: `Queued but never sent: ${entry.text}`,
|
|
213
213
|
};
|
|
214
214
|
return downgraded;
|
|
215
215
|
}
|
|
@@ -218,9 +218,6 @@ export function normalizeLoadedEntries(entries) {
|
|
|
218
218
|
}
|
|
219
219
|
export function loadSession(filePath) {
|
|
220
220
|
const raw = readFileSync(filePath, 'utf-8');
|
|
221
|
-
const parsed = JSON.parse(raw);
|
|
222
|
-
if (!parsed || !Array.isArray(parsed.messages) || !Array.isArray(parsed.entries)) {
|
|
223
|
-
throw new Error('Invalid session file (missing messages/entries).');
|
|
224
|
-
}
|
|
221
|
+
const parsed = validateSession(JSON.parse(raw));
|
|
225
222
|
return { ...parsed, entries: normalizeLoadedEntries(parsed.entries) };
|
|
226
223
|
}
|
package/dist/session/store.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
* command the docs recommend. `list` and `prune` therefore see only files
|
|
19
19
|
* carrying `meta.id`; `show` and `rm` accept any id, because the user named it.
|
|
20
20
|
*/
|
|
21
|
+
import type { ModelRef } from '@aragon-agent/core';
|
|
21
22
|
import { type SavedSession, type SessionMeta } from './persist.js';
|
|
22
23
|
/**
|
|
23
24
|
* The id is a FILENAME under the user's home directory, and the caller supplying
|
|
@@ -84,7 +85,9 @@ export declare function readSessionFile(path: string): SavedSession | null;
|
|
|
84
85
|
export declare function resolveSessionRef(idOrPath: string, cwd: string): string | null;
|
|
85
86
|
export interface WriteSessionInput {
|
|
86
87
|
id: string;
|
|
87
|
-
session: Omit<SavedSession, 'version' | 'savedAt' | 'meta'
|
|
88
|
+
session: Omit<SavedSession, 'version' | 'savedAt' | 'meta' | 'model'> & {
|
|
89
|
+
model: ModelRef;
|
|
90
|
+
};
|
|
88
91
|
meta: SessionMeta;
|
|
89
92
|
}
|
|
90
93
|
/**
|