aiden-runtime 4.0.2 → 4.1.0
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/README.md +11 -7
- package/config/hardware.json +2 -2
- package/dist/api/server.js +50 -52
- package/dist/cli/v4/aidenCLI.js +421 -5
- package/dist/cli/v4/aidenPrompt.js +317 -0
- package/dist/cli/v4/box.js +105 -39
- package/dist/cli/v4/callbacks.js +39 -6
- package/dist/cli/v4/chatSession.js +256 -55
- package/dist/cli/v4/citationFooter.js +97 -0
- package/dist/cli/v4/commands/channel.js +656 -0
- package/dist/cli/v4/commands/clear.js +1 -1
- package/dist/cli/v4/commands/compress.js +1 -1
- package/dist/cli/v4/commands/cron.js +44 -16
- package/dist/cli/v4/commands/fanout.js +236 -0
- package/dist/cli/v4/commands/help.js +15 -4
- package/dist/cli/v4/commands/history.js +84 -0
- package/dist/cli/v4/commands/index.js +16 -1
- package/dist/cli/v4/commands/mcp.js +358 -0
- package/dist/cli/v4/commands/show.js +43 -0
- package/dist/cli/v4/commands/skills.js +169 -4
- package/dist/cli/v4/commands/status.js +84 -0
- package/dist/cli/v4/commands/subagent.js +78 -0
- package/dist/cli/v4/commands/verbose.js +1 -1
- package/dist/cli/v4/commands/voice.js +218 -0
- package/dist/cli/v4/cronCli.js +103 -0
- package/dist/cli/v4/display.js +297 -13
- package/dist/cli/v4/doctor.js +41 -0
- package/dist/cli/v4/envSources.js +105 -0
- package/dist/cli/v4/ghostMatch.js +74 -0
- package/dist/cli/v4/historyStore.js +163 -0
- package/dist/cli/v4/pasteCompression.js +124 -0
- package/dist/cli/v4/pasteIntercept.js +203 -0
- package/dist/cli/v4/replyRenderer.js +209 -0
- package/dist/cli/v4/resizeGuard.js +92 -0
- package/dist/cli/v4/shellInterpolation.js +139 -0
- package/dist/cli/v4/skinEngine.js +21 -1
- package/dist/cli/v4/streamingPrefix.js +121 -0
- package/dist/cli/v4/syntaxHighlight.js +345 -0
- package/dist/cli/v4/table.js +216 -0
- package/dist/cli/v4/themeDetect.js +81 -0
- package/dist/cli/v4/uiBuild.js +74 -0
- package/dist/cli/v4/voiceCli.js +113 -0
- package/dist/cli/v4/voicePromptApi.js +196 -0
- package/dist/core/channels/discord.js +16 -10
- package/dist/core/channels/email.js +13 -9
- package/dist/core/channels/imessage.js +13 -9
- package/dist/core/channels/manager.js +25 -7
- package/dist/core/channels/pdf-extract.js +180 -0
- package/dist/core/channels/photo-vision.js +157 -0
- package/dist/core/channels/signal.js +11 -7
- package/dist/core/channels/slack.js +13 -10
- package/dist/core/channels/telegram-commands.js +154 -0
- package/dist/core/channels/telegram-groups.js +198 -0
- package/dist/core/channels/telegram-rate-limit.js +124 -0
- package/dist/core/channels/telegram.js +1980 -0
- package/dist/core/channels/twilio.js +11 -7
- package/dist/core/channels/webhook.js +9 -5
- package/dist/core/channels/whatsapp.js +15 -11
- package/dist/core/channels/whisper-transcribe.js +163 -0
- package/dist/core/cronManager.js +33 -294
- package/dist/core/gateway.js +29 -8
- package/dist/core/playwrightBridge.js +90 -0
- package/dist/core/v4/aidenAgent.js +35 -0
- package/dist/core/v4/auxiliaryClient.js +2 -2
- package/dist/core/v4/cron/atomicWrite.js +18 -4
- package/dist/core/v4/cron/cronExecute.js +300 -0
- package/dist/core/v4/cron/cronManager.js +502 -0
- package/dist/core/v4/cron/cronState.js +314 -0
- package/dist/core/v4/cron/cronTick.js +90 -0
- package/dist/core/v4/cron/diagnostics.js +104 -0
- package/dist/core/v4/cron/graceWindow.js +79 -0
- package/dist/core/v4/logger/factory.js +110 -0
- package/dist/core/v4/logger/index.js +22 -0
- package/dist/core/v4/logger/logger.js +101 -0
- package/dist/core/v4/logger/sinks/fileSink.js +110 -0
- package/dist/core/v4/logger/sinks/multiSink.js +43 -0
- package/dist/core/v4/logger/sinks/nullSink.js +53 -0
- package/dist/core/v4/logger/sinks/stdSink.js +81 -0
- package/dist/core/v4/mcp/server/diagnostics.js +40 -0
- package/dist/core/v4/mcp/server/skillBridge.js +94 -0
- package/dist/core/v4/mcp/server/stdioServer.js +119 -0
- package/dist/core/v4/mcp/server/toolBridge.js +168 -0
- package/dist/core/v4/platformPaths.js +105 -0
- package/dist/core/v4/providerFallback.js +25 -0
- package/dist/core/v4/skillLoader.js +21 -5
- package/dist/core/v4/skillMining/candidateStore.js +164 -0
- package/dist/core/v4/skillMining/extractorPrompt.js +111 -0
- package/dist/core/v4/skillMining/proposalBuilder.js +139 -0
- package/dist/core/v4/skillMining/skillMiner.js +191 -0
- package/dist/core/v4/skillMining/traceFingerprint.js +51 -0
- package/dist/core/v4/subagent/budget.js +76 -0
- package/dist/core/v4/subagent/diagnostics.js +22 -0
- package/dist/core/v4/subagent/fanout.js +216 -0
- package/dist/core/v4/subagent/merger.js +148 -0
- package/dist/core/v4/subagent/providerRotation.js +54 -0
- package/dist/core/v4/voice/audioStream.js +373 -0
- package/dist/core/v4/voice/cliVoice.js +393 -0
- package/dist/core/v4/voice/diagnostics.js +66 -0
- package/dist/core/v4/voice/ttsStream.js +193 -0
- package/dist/core/version.js +1 -1
- package/dist/core/visionAnalyze.js +291 -90
- package/dist/core/voice/audio.js +61 -5
- package/dist/core/voice/audioBackend.js +134 -0
- package/dist/core/voice/stt.js +61 -6
- package/dist/core/voice/tts.js +19 -3
- package/dist/tools/v4/index.js +32 -1
- package/dist/tools/v4/subagent/subagentFanout.js +166 -0
- package/package.json +11 -2
package/dist/cli/v4/display.js
CHANGED
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
21
|
exports.Display = exports.SPINNER_PHRASES = exports.TOOL_ICONS = void 0;
|
|
22
22
|
exports.iconForTool = iconForTool;
|
|
23
|
+
exports.detectConfiguredChannels = detectConfiguredChannels;
|
|
24
|
+
exports.summarizeConfiguredChannels = summarizeConfiguredChannels;
|
|
25
|
+
exports.summarizeChannelState = summarizeChannelState;
|
|
26
|
+
exports.voiceIndicator = voiceIndicator;
|
|
23
27
|
exports.previewToolArgs = previewToolArgs;
|
|
24
28
|
exports.formatToolDuration = formatToolDuration;
|
|
25
29
|
exports.formatCompactTokens = formatCompactTokens;
|
|
@@ -32,6 +36,12 @@ const marked_1 = require("marked");
|
|
|
32
36
|
const TerminalRenderer = require('marked-terminal').default ?? require('marked-terminal');
|
|
33
37
|
const skinEngine_1 = require("./skinEngine");
|
|
34
38
|
const box_1 = require("./box");
|
|
39
|
+
// Phase v4.1-reply-formatting: skin-aware markdown renderer that
|
|
40
|
+
// replaces marked-terminal's defaults with structured headers, lists,
|
|
41
|
+
// code blocks, blockquotes, and links.
|
|
42
|
+
const replyRenderer_1 = require("./replyRenderer");
|
|
43
|
+
// Optional "Sources" footer when AIDEN_CITATIONS=1 (default off).
|
|
44
|
+
const citationFooter_1 = require("./citationFooter");
|
|
35
45
|
/**
|
|
36
46
|
* Phase 26.2.7 — category emoji icons for the tool-row prefix when
|
|
37
47
|
* `AIDEN_UI_ICONS=1` is set in the environment. Default OFF (the
|
|
@@ -126,6 +136,74 @@ exports.SPINNER_PHRASES = [
|
|
|
126
136
|
'Smelting',
|
|
127
137
|
'Conjuring',
|
|
128
138
|
];
|
|
139
|
+
/**
|
|
140
|
+
* Phase v4.1-1 — boot card "channels" pill helpers. The CLI process
|
|
141
|
+
* doesn't actually run channel adapters (those live in the API server)
|
|
142
|
+
* so we report what we *can* honestly know from the environment: how
|
|
143
|
+
* many of the nine channel adapters have their credentials present.
|
|
144
|
+
*
|
|
145
|
+
* `summarizeConfiguredChannels` returns a render-ready label like
|
|
146
|
+
* `"3 configured (incl. telegram)"` for the Environment column.
|
|
147
|
+
*/
|
|
148
|
+
const CHANNEL_ENV_VARS = [
|
|
149
|
+
{ id: 'telegram', vars: ['TELEGRAM_BOT_TOKEN'] },
|
|
150
|
+
{ id: 'discord', vars: ['DISCORD_BOT_TOKEN'] },
|
|
151
|
+
{ id: 'slack', vars: ['SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN'] },
|
|
152
|
+
{ id: 'whatsapp', vars: ['WHATSAPP_BUSINESS_API_KEY'] },
|
|
153
|
+
{ id: 'twilio', vars: ['TWILIO_AUTH_TOKEN'] },
|
|
154
|
+
{ id: 'imessage', vars: ['BLUEBUBBLES_PASSWORD'] },
|
|
155
|
+
{ id: 'email', vars: ['EMAIL_IMAP_PASSWORD', 'EMAIL_SMTP_PASSWORD'] },
|
|
156
|
+
];
|
|
157
|
+
function detectConfiguredChannels(env = process.env) {
|
|
158
|
+
const ids = [];
|
|
159
|
+
for (const c of CHANNEL_ENV_VARS) {
|
|
160
|
+
if (c.vars.some((v) => typeof env[v] === 'string' && env[v].trim() !== '')) {
|
|
161
|
+
ids.push(c.id);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { total: ids.length, ids, telegram: ids.includes('telegram') };
|
|
165
|
+
}
|
|
166
|
+
function summarizeConfiguredChannels(detection = detectConfiguredChannels()) {
|
|
167
|
+
if (detection.total === 0)
|
|
168
|
+
return '0 configured';
|
|
169
|
+
const suffix = detection.telegram ? ' (incl. telegram)' : '';
|
|
170
|
+
return `${detection.total} configured${suffix}`;
|
|
171
|
+
}
|
|
172
|
+
function summarizeChannelState(probe, envFallback = detectConfiguredChannels()) {
|
|
173
|
+
// No live probe (e.g. test harness with no manager) → env-only count.
|
|
174
|
+
if (!probe) {
|
|
175
|
+
if (envFallback.total === 0) {
|
|
176
|
+
return '0 configured (run /channel telegram add to enable)';
|
|
177
|
+
}
|
|
178
|
+
return `${envFallback.total} configured${envFallback.telegram ? ' (incl. telegram)' : ''}`;
|
|
179
|
+
}
|
|
180
|
+
// Phase v4.1-1.2 — conflict takes priority over the generic active /
|
|
181
|
+
// offline split. If any adapter is in the conflict state, surface
|
|
182
|
+
// that explicitly so the user has an unambiguous remediation hint.
|
|
183
|
+
const conflicted = probe.adapters.filter((a) => a.state === 'conflict');
|
|
184
|
+
if (conflicted.length > 0) {
|
|
185
|
+
const names = conflicted.map((a) => a.id).join(', ');
|
|
186
|
+
return `${conflicted.length} degraded: ${names} (conflict — /channel telegram takeover)`;
|
|
187
|
+
}
|
|
188
|
+
const active = probe.adapters.filter((a) => a.healthy);
|
|
189
|
+
const inactive = probe.adapters.filter((a) => !a.healthy);
|
|
190
|
+
if (active.length === 0) {
|
|
191
|
+
if (envFallback.total === 0) {
|
|
192
|
+
return '0 configured (run /channel telegram add to enable)';
|
|
193
|
+
}
|
|
194
|
+
// Token in env but adapter not healthy — frame it as offline so
|
|
195
|
+
// the user knows /channel telegram status is the next step.
|
|
196
|
+
const offlineNames = inactive
|
|
197
|
+
.filter((a) => envFallback.ids.includes(a.id))
|
|
198
|
+
.map((a) => a.id)
|
|
199
|
+
.join(', ');
|
|
200
|
+
return offlineNames
|
|
201
|
+
? `${envFallback.total} configured: ${offlineNames} (offline — /channel telegram status)`
|
|
202
|
+
: `${envFallback.total} configured`;
|
|
203
|
+
}
|
|
204
|
+
const parts = active.map((a) => (a.botHandle ? `${a.id} (@${a.botHandle})` : a.id));
|
|
205
|
+
return `${active.length} active: ${parts.join(', ')}`;
|
|
206
|
+
}
|
|
129
207
|
const AIDEN_BANNER = String.raw `
|
|
130
208
|
█████╗ ██╗██████╗ ███████╗███╗ ██╗
|
|
131
209
|
██╔══██╗██║██╔══██╗██╔════╝████╗ ██║
|
|
@@ -144,6 +222,14 @@ class Display {
|
|
|
144
222
|
// starts on its own line.
|
|
145
223
|
this.streamHeaderShown = false;
|
|
146
224
|
this.streamLastEndedNewline = false;
|
|
225
|
+
// Phase v4.1-reply-formatting: track the running buffered stream
|
|
226
|
+
// so streamComplete can re-render it as structured markdown
|
|
227
|
+
// (headers / lists / code blocks / blockquotes) once the full
|
|
228
|
+
// body is known. During streaming the raw text remains visible
|
|
229
|
+
// — the post-stream pass clears it via cursor-up + erase-line and
|
|
230
|
+
// reprints the formatted output.
|
|
231
|
+
this.streamBuffer = '';
|
|
232
|
+
this.streamLineCount = 0;
|
|
147
233
|
this.skin = opts.skin ?? (0, skinEngine_1.getSkinEngine)();
|
|
148
234
|
this.out = opts.stdout ?? process.stdout;
|
|
149
235
|
this.err = opts.stderr ?? process.stderr;
|
|
@@ -320,10 +406,13 @@ class Display {
|
|
|
320
406
|
* `cols() >= 80`, stacked vertically below that. Title in brand,
|
|
321
407
|
* keys in muted (padded to 11 visible chars), values in `agent`.
|
|
322
408
|
*/
|
|
323
|
-
twoColumnBlock(left, right) {
|
|
409
|
+
twoColumnBlock(left, right, opts = {}) {
|
|
324
410
|
const sk = this.skin;
|
|
325
411
|
const cols = this.cols();
|
|
326
|
-
|
|
412
|
+
// Tier-3.1b: callers can raise the side-by-side threshold (boot
|
|
413
|
+
// card prefers stacked at 70-119 cols, side-by-side only at ≥120).
|
|
414
|
+
// Default 80 preserves prior behaviour for any other caller.
|
|
415
|
+
const stacked = cols < (opts.sideBySideThreshold ?? 80);
|
|
327
416
|
const indent = ' ';
|
|
328
417
|
const KEY_PAD = 11;
|
|
329
418
|
const renderRows = (sec) => {
|
|
@@ -395,13 +484,10 @@ class Display {
|
|
|
395
484
|
const val = (s) => sk.applyColors(s, 'agent');
|
|
396
485
|
const heart = sk.applyColors('♥', 'brand');
|
|
397
486
|
if (this.cols() < 80) {
|
|
398
|
-
//
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
` ${lab('Web:')} ${val('aiden.taracod.com')}`,
|
|
403
|
-
` ${lab('Contact:')} ${val('contact@taracod.com')}`,
|
|
404
|
-
].join('\n');
|
|
487
|
+
// Tier-3.1b: single-line credits at narrow widths so the boot
|
|
488
|
+
// card stays compact. The 4-line plain fallback shipped earlier
|
|
489
|
+
// wastes vertical space when terminals already squeeze content.
|
|
490
|
+
return ` ${heart} ${m('built solo · github.com/taracodlabs/aiden · aiden.taracod.com')}`;
|
|
405
491
|
}
|
|
406
492
|
// Parchment.
|
|
407
493
|
const INTERIOR = 63;
|
|
@@ -477,6 +563,74 @@ class Display {
|
|
|
477
563
|
const elapsed = sk.applyColors(formatElapsedShort(args.elapsedMs), 'muted');
|
|
478
564
|
return ` ${provModel}${SEP}${ctxSeg}${SEP}${elapsed}`;
|
|
479
565
|
}
|
|
566
|
+
/**
|
|
567
|
+
* Tier-3.1 (v4.1-tier3.1): pre-prompt status line.
|
|
568
|
+
*
|
|
569
|
+
* Single-line summary written ABOVE the input prompt on every
|
|
570
|
+
* fresh turn. Format (full, ≥76 cols):
|
|
571
|
+
*
|
|
572
|
+
* <provider>:<model> · ctx <N>/<M>k · MCP <state> · cron <state>
|
|
573
|
+
*
|
|
574
|
+
* Width-tier degrade:
|
|
575
|
+
* <52 cols → only `<provider>:<model> · ctx N/Mk`
|
|
576
|
+
* <76 cols → drop voice indicator
|
|
577
|
+
* ≥76 cols → full
|
|
578
|
+
*
|
|
579
|
+
* MCP serve mode: this helper is a pure builder; callers MUST
|
|
580
|
+
* gate the actual write on `isMcpServeMode()` from
|
|
581
|
+
* `cli/v4/uiBuild.ts`. The function itself never writes.
|
|
582
|
+
*/
|
|
583
|
+
renderStatusLine(args) {
|
|
584
|
+
const sk = this.skin;
|
|
585
|
+
const cols = args.cols ?? this.cols();
|
|
586
|
+
const SEP = sk.applyColors(' · ', 'muted');
|
|
587
|
+
const colourForState = (s) => {
|
|
588
|
+
if (s === 'active')
|
|
589
|
+
return 'success';
|
|
590
|
+
if (s === 'broken')
|
|
591
|
+
return 'error';
|
|
592
|
+
return 'muted';
|
|
593
|
+
};
|
|
594
|
+
const glyphForState = (s) => {
|
|
595
|
+
if (s === 'active')
|
|
596
|
+
return '✓'; // ✓
|
|
597
|
+
if (s === 'broken')
|
|
598
|
+
return '✗'; // ✗
|
|
599
|
+
return '-';
|
|
600
|
+
};
|
|
601
|
+
const provModel = sk.applyColors(args.provider, 'muted') +
|
|
602
|
+
sk.applyColors(':', 'muted') +
|
|
603
|
+
sk.applyColors(args.model, 'agent');
|
|
604
|
+
const ctxSeg = (() => {
|
|
605
|
+
if (args.ctxMax == null || args.ctxUsed == null)
|
|
606
|
+
return '';
|
|
607
|
+
const usedK = Math.round(args.ctxUsed / 1000);
|
|
608
|
+
const maxK = Math.max(1, Math.round(args.ctxMax / 1000));
|
|
609
|
+
return sk.applyColors(`ctx ${usedK}/${maxK}k`, 'muted');
|
|
610
|
+
})();
|
|
611
|
+
const mcpSeg = args.mcpState
|
|
612
|
+
? sk.applyColors(`MCP ${glyphForState(args.mcpState)}`, colourForState(args.mcpState))
|
|
613
|
+
: '';
|
|
614
|
+
const cronSeg = args.cronState
|
|
615
|
+
? sk.applyColors(`cron ${glyphForState(args.cronState)}`, colourForState(args.cronState))
|
|
616
|
+
: '';
|
|
617
|
+
const voiceSeg = args.voiceRecording
|
|
618
|
+
? sk.applyColors('[REC]', 'error')
|
|
619
|
+
: '';
|
|
620
|
+
// Compose with width-tier degrade.
|
|
621
|
+
const segs = [provModel];
|
|
622
|
+
if (ctxSeg)
|
|
623
|
+
segs.push(ctxSeg);
|
|
624
|
+
if (cols >= 52) {
|
|
625
|
+
if (mcpSeg)
|
|
626
|
+
segs.push(mcpSeg);
|
|
627
|
+
if (cronSeg)
|
|
628
|
+
segs.push(cronSeg);
|
|
629
|
+
}
|
|
630
|
+
if (cols >= 76 && voiceSeg)
|
|
631
|
+
segs.push(voiceSeg);
|
|
632
|
+
return ' ' + segs.join(SEP);
|
|
633
|
+
}
|
|
480
634
|
/**
|
|
481
635
|
* Optional provider-switch indicator line — emitted only when this
|
|
482
636
|
* turn ran on a different provider than the previous one. Format
|
|
@@ -657,14 +811,24 @@ class Display {
|
|
|
657
811
|
const arrow = sk.getActive().glyphs?.arrow ?? '>';
|
|
658
812
|
return `${sk.applyColors(arrow, 'tool')} ${sk.applyColors(name, 'tool')} ${sk.applyColors(serialized, 'muted')}`;
|
|
659
813
|
}
|
|
660
|
-
/**
|
|
814
|
+
/**
|
|
815
|
+
* Render markdown to ANSI via the v4.1-reply-formatting renderer
|
|
816
|
+
* (skin-aware headers / lists / code / quotes / links). Falls back
|
|
817
|
+
* to the marked-terminal default config if the structured renderer
|
|
818
|
+
* is unavailable, then raw text as a last resort.
|
|
819
|
+
*/
|
|
661
820
|
markdown(text) {
|
|
662
821
|
try {
|
|
663
|
-
|
|
664
|
-
return typeof out === 'string' ? out : String(out);
|
|
822
|
+
return (0, replyRenderer_1.getReplyRenderer)().render(text);
|
|
665
823
|
}
|
|
666
824
|
catch {
|
|
667
|
-
|
|
825
|
+
try {
|
|
826
|
+
const out = marked_1.marked.parse(text);
|
|
827
|
+
return typeof out === 'string' ? out : String(out);
|
|
828
|
+
}
|
|
829
|
+
catch {
|
|
830
|
+
return text;
|
|
831
|
+
}
|
|
668
832
|
}
|
|
669
833
|
}
|
|
670
834
|
/** Format a user turn (e.g. echoed back from history). */
|
|
@@ -809,9 +973,18 @@ class Display {
|
|
|
809
973
|
// open identically.
|
|
810
974
|
this.out.write(this.agentHeader());
|
|
811
975
|
this.streamHeaderShown = true;
|
|
976
|
+
this.streamBuffer = '';
|
|
977
|
+
this.streamLineCount = 0;
|
|
812
978
|
}
|
|
813
979
|
this.out.write(text);
|
|
814
980
|
this.streamLastEndedNewline = text.endsWith('\n');
|
|
981
|
+
// Phase v4.1-reply-formatting: track buffer + line count for the
|
|
982
|
+
// post-stream re-render. We count newlines in the OUTGOING bytes
|
|
983
|
+
// so the eraser later knows how many rows to clear.
|
|
984
|
+
this.streamBuffer += text;
|
|
985
|
+
for (let i = 0; i < text.length; i += 1)
|
|
986
|
+
if (text[i] === '\n')
|
|
987
|
+
this.streamLineCount += 1;
|
|
815
988
|
}
|
|
816
989
|
/**
|
|
817
990
|
* Mark the end of a streaming turn. Adds a trailing newline if the
|
|
@@ -824,8 +997,68 @@ class Display {
|
|
|
824
997
|
return;
|
|
825
998
|
if (!this.streamLastEndedNewline)
|
|
826
999
|
this.out.write('\n');
|
|
1000
|
+
// Phase v4.1-reply-formatting: re-render the buffered stream as
|
|
1001
|
+
// structured markdown — but ONLY when stdout is a TTY and the
|
|
1002
|
+
// buffer actually contains markdown structure worth rendering.
|
|
1003
|
+
// Plain prose with no headers / lists / fences gets left alone
|
|
1004
|
+
// (no flicker, identical output). Otherwise we erase the raw
|
|
1005
|
+
// streamed body via cursor-up + erase-line and reprint via the
|
|
1006
|
+
// skin-aware renderer.
|
|
1007
|
+
const buffered = this.streamBuffer;
|
|
1008
|
+
const lines = this.streamLineCount;
|
|
1009
|
+
this.streamBuffer = '';
|
|
1010
|
+
this.streamLineCount = 0;
|
|
827
1011
|
this.streamHeaderShown = false;
|
|
828
1012
|
this.streamLastEndedNewline = false;
|
|
1013
|
+
if (!this.out.isTTY)
|
|
1014
|
+
return;
|
|
1015
|
+
if (process.env.AIDEN_NO_REFORMAT === '1')
|
|
1016
|
+
return;
|
|
1017
|
+
// Cheap heuristic: only re-render when there's structure that
|
|
1018
|
+
// benefits from formatting. Avoids flicker on short prose replies.
|
|
1019
|
+
const hasStructure = /^#{1,6}\s/m.test(buffered) ||
|
|
1020
|
+
/^\s*[-*+]\s/m.test(buffered) ||
|
|
1021
|
+
/^\s*\d+\.\s/m.test(buffered) ||
|
|
1022
|
+
/^>\s/m.test(buffered) ||
|
|
1023
|
+
/```/.test(buffered);
|
|
1024
|
+
if (!hasStructure)
|
|
1025
|
+
return;
|
|
1026
|
+
try {
|
|
1027
|
+
// Erase the raw streamed body in place. We wrote `lines + 1`
|
|
1028
|
+
// rows (header + body) — the header (`┃ Aiden`) stays, so we
|
|
1029
|
+
// walk back `lines` rows and clear each.
|
|
1030
|
+
// `\x1b[<n>F` = cursor-up-and-to-column-0 N times.
|
|
1031
|
+
// `\x1b[J` = erase from cursor to end of screen.
|
|
1032
|
+
if (lines > 0) {
|
|
1033
|
+
this.out.write(`\x1b[${lines}F\x1b[J`);
|
|
1034
|
+
}
|
|
1035
|
+
const formatted = this.markdown(buffered).trimEnd();
|
|
1036
|
+
const indented = formatted
|
|
1037
|
+
.split('\n')
|
|
1038
|
+
.map((ln) => (ln ? ` ${ln}` : ''))
|
|
1039
|
+
.join('\n');
|
|
1040
|
+
this.out.write(indented + '\n');
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
// If anything goes wrong with the re-render, leave the raw
|
|
1044
|
+
// streamed text in place — graceful degradation beats flicker
|
|
1045
|
+
// + corrupted output.
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
/**
|
|
1049
|
+
* Phase v4.1-reply-formatting: render the optional "Sources"
|
|
1050
|
+
* footer when AIDEN_CITATIONS=1 and the trace has fetch-class
|
|
1051
|
+
* tool calls. Pure write; safe to call after a turn completes
|
|
1052
|
+
* regardless of streaming/non-streaming. No-op when the env gate
|
|
1053
|
+
* is off or no sources surface.
|
|
1054
|
+
*/
|
|
1055
|
+
printCitationFooter(trace) {
|
|
1056
|
+
const footer = (0, citationFooter_1.renderCitationFooter)(trace);
|
|
1057
|
+
if (!footer)
|
|
1058
|
+
return;
|
|
1059
|
+
if (!this.out.isTTY)
|
|
1060
|
+
return;
|
|
1061
|
+
this.out.write(footer);
|
|
829
1062
|
}
|
|
830
1063
|
/**
|
|
831
1064
|
* Inline tool indicator. Printed between deltas when a tool call
|
|
@@ -842,6 +1075,57 @@ class Display {
|
|
|
842
1075
|
}
|
|
843
1076
|
}
|
|
844
1077
|
exports.Display = Display;
|
|
1078
|
+
/**
|
|
1079
|
+
* Render a voice-mode status line. Pure builder — caller writes the
|
|
1080
|
+
* result via Display.streamPartial / direct stdout. Includes a
|
|
1081
|
+
* RMS-driven block bar when state is `recording`. The bar uses 8
|
|
1082
|
+
* unicode block-fill levels (▏ to █) over a 0..1500 RMS range,
|
|
1083
|
+
* which covers the practical loud-speech ceiling without saturating
|
|
1084
|
+
* for any reasonable mic preamp.
|
|
1085
|
+
*
|
|
1086
|
+
* Tier-3.1 (v4.1-tier3.1): replaced 🎤 / 🔊 emoji with text-state
|
|
1087
|
+
* badges `[REC]` (recording, error/red) and `[PLAY]` (speaking,
|
|
1088
|
+
* success/green). Idle/listening/transcribing get the neutral
|
|
1089
|
+
* `[VOX]` badge in muted colour. Same 4-char inner width keeps
|
|
1090
|
+
* subsequent column alignment intact.
|
|
1091
|
+
*
|
|
1092
|
+
* Examples:
|
|
1093
|
+
* voiceIndicator('idle') → '[VOX] idle (Space to talk)'
|
|
1094
|
+
* voiceIndicator('listening') → '[VOX] listening...'
|
|
1095
|
+
* voiceIndicator('recording', 800) → '[REC] ▌▌▌▌▌▌ recording (Space to stop, Esc to cancel)'
|
|
1096
|
+
* voiceIndicator('transcribing') → '[VOX] transcribing...'
|
|
1097
|
+
* voiceIndicator('speaking') → '[PLAY] speaking...'
|
|
1098
|
+
*/
|
|
1099
|
+
function voiceIndicator(state, rms = 0) {
|
|
1100
|
+
const skin = (0, skinEngine_1.getSkinEngine)();
|
|
1101
|
+
const recBadge = skin.applyColors('[REC]', 'error');
|
|
1102
|
+
const playBadge = skin.applyColors('[PLAY]', 'success');
|
|
1103
|
+
const voxBadge = skin.applyColors('[VOX]', 'muted');
|
|
1104
|
+
switch (state) {
|
|
1105
|
+
case 'idle':
|
|
1106
|
+
return `${voxBadge} idle (Space to talk)`;
|
|
1107
|
+
case 'listening':
|
|
1108
|
+
return `${voxBadge} listening...`;
|
|
1109
|
+
case 'recording': {
|
|
1110
|
+
const bar = renderRmsBar(rms);
|
|
1111
|
+
return `${recBadge} ${bar} recording (Space to stop, Esc to cancel)`;
|
|
1112
|
+
}
|
|
1113
|
+
case 'transcribing':
|
|
1114
|
+
return `${voxBadge} transcribing...`;
|
|
1115
|
+
case 'speaking':
|
|
1116
|
+
return `${playBadge} speaking...`;
|
|
1117
|
+
default:
|
|
1118
|
+
return `${voxBadge} ${state}`;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
const BAR_WIDTH = 12;
|
|
1122
|
+
const BAR_FULL_RMS = 1500;
|
|
1123
|
+
/** RMS-driven horizontal block bar. 0..BAR_FULL_RMS → 0..BAR_WIDTH chars. */
|
|
1124
|
+
function renderRmsBar(rms) {
|
|
1125
|
+
const safe = Math.max(0, Math.min(rms, BAR_FULL_RMS));
|
|
1126
|
+
const filled = Math.round((safe / BAR_FULL_RMS) * BAR_WIDTH);
|
|
1127
|
+
return '▌'.repeat(filled) + ' '.repeat(BAR_WIDTH - filled);
|
|
1128
|
+
}
|
|
845
1129
|
// ── Phase 23.5 — tool row helpers ─────────────────────────────────────
|
|
846
1130
|
/** Width the tool name is padded to so brackets line up across rows. */
|
|
847
1131
|
const TOOL_ROW_NAME_PAD = 16;
|
package/dist/cli/v4/doctor.js
CHANGED
|
@@ -34,6 +34,7 @@ exports.checkNpxAvailable = checkNpxAvailable;
|
|
|
34
34
|
exports.checkSkillsDir = checkSkillsDir;
|
|
35
35
|
exports.checkBundledManifest = checkBundledManifest;
|
|
36
36
|
exports.checkPlatformPaths = checkPlatformPaths;
|
|
37
|
+
exports.checkAudioBackend = checkAudioBackend;
|
|
37
38
|
exports.checkLicense = checkLicense;
|
|
38
39
|
exports.checkUpdate = checkUpdate;
|
|
39
40
|
exports.checkLogsWritable = checkLogsWritable;
|
|
@@ -48,6 +49,7 @@ const paths_1 = require("../../core/v4/paths");
|
|
|
48
49
|
const license_1 = require("../../core/v4/license");
|
|
49
50
|
const checkUpdate_1 = require("../../core/v4/update/checkUpdate");
|
|
50
51
|
const box_1 = require("./box");
|
|
52
|
+
const audioBackend_1 = require("../../core/voice/audioBackend");
|
|
51
53
|
const DEFAULT_TIMEOUT_MS = 3000;
|
|
52
54
|
/** Wrap a promise with a timeout. The timed-out path resolves to the fallback result. */
|
|
53
55
|
async function withTimeout(p, ms, fallback) {
|
|
@@ -421,6 +423,44 @@ async function checkPlatformPaths(paths) {
|
|
|
421
423
|
};
|
|
422
424
|
}
|
|
423
425
|
}
|
|
426
|
+
/**
|
|
427
|
+
* Phase v4.1-cross-platform: probe per-OS audio backends so a user
|
|
428
|
+
* who installs Aiden fresh on Linux/macOS gets a clear "install sox"
|
|
429
|
+
* pointer instead of a stack trace the first time they run /voice.
|
|
430
|
+
*
|
|
431
|
+
* Reports as INFO not FAILURE — the agent loop works fine without
|
|
432
|
+
* voice support; we just want to surface the install hint.
|
|
433
|
+
*/
|
|
434
|
+
async function checkAudioBackend() {
|
|
435
|
+
const t0 = Date.now();
|
|
436
|
+
const playback = (0, audioBackend_1.detectBackend)('playback');
|
|
437
|
+
const record = (0, audioBackend_1.detectBackend)('record');
|
|
438
|
+
const known = {
|
|
439
|
+
playback: (0, audioBackend_1.listKnownBackends)('playback').map((b) => b.label),
|
|
440
|
+
record: (0, audioBackend_1.listKnownBackends)('record').map((b) => b.label),
|
|
441
|
+
};
|
|
442
|
+
if (playback && record) {
|
|
443
|
+
return {
|
|
444
|
+
name: 'audio backend',
|
|
445
|
+
passed: true,
|
|
446
|
+
message: `${process.platform}: playback=${playback.label} · record=${record.label}`,
|
|
447
|
+
durationMs: Date.now() - t0,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
// Pass=true — informational. The suggestion carries the fix.
|
|
451
|
+
const missing = [];
|
|
452
|
+
if (!playback)
|
|
453
|
+
missing.push((0, audioBackend_1.missingBackendMessage)('playback'));
|
|
454
|
+
if (!record)
|
|
455
|
+
missing.push((0, audioBackend_1.missingBackendMessage)('record'));
|
|
456
|
+
return {
|
|
457
|
+
name: 'audio backend',
|
|
458
|
+
passed: true,
|
|
459
|
+
message: `${process.platform}: voice features will not work — backends missing (known: ${[...new Set([...known.playback, ...known.record])].join(', ') || 'none'})`,
|
|
460
|
+
suggestion: missing.join(' || '),
|
|
461
|
+
durationMs: Date.now() - t0,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
424
464
|
/**
|
|
425
465
|
* Phase 20 Task 7: license-server reachability + local cache state.
|
|
426
466
|
* `/doctor` shouldn't block when offline — we treat both "no local cache
|
|
@@ -586,6 +626,7 @@ async function runDoctor(opts = {}) {
|
|
|
586
626
|
results.push(await checkBundledManifest(paths));
|
|
587
627
|
results.push(await checkPlatformPaths(paths));
|
|
588
628
|
results.push(await checkLogsWritable(paths));
|
|
629
|
+
results.push(await checkAudioBackend());
|
|
589
630
|
// Phase 20 Task 7: license + update health.
|
|
590
631
|
results.push(await checkLicense({ paths, fetchImpl, timeoutMs }));
|
|
591
632
|
results.push(await checkUpdate({ paths, installedVersion, timeoutMs }));
|
|
@@ -33,9 +33,13 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.KNOWN_PROVIDER_KEYS = void 0;
|
|
36
37
|
exports.loadAidenEnvFile = loadAidenEnvFile;
|
|
37
38
|
exports.getEnvSource = getEnvSource;
|
|
38
39
|
exports.__resetEnvSources = __resetEnvSources;
|
|
40
|
+
exports.resolveAidenInstallDir = resolveAidenInstallDir;
|
|
41
|
+
exports.loadMcpEnvSources = loadMcpEnvSources;
|
|
42
|
+
exports.describeProviderKeys = describeProviderKeys;
|
|
39
43
|
/**
|
|
40
44
|
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
41
45
|
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
@@ -59,6 +63,7 @@ exports.__resetEnvSources = __resetEnvSources;
|
|
|
59
63
|
* - 'unset' — not in process.env
|
|
60
64
|
*/
|
|
61
65
|
const fs = __importStar(require("node:fs"));
|
|
66
|
+
const path = __importStar(require("node:path"));
|
|
62
67
|
const ENV_SOURCE_TAG = Symbol.for('aiden.envSource');
|
|
63
68
|
function getMap() {
|
|
64
69
|
let m = globalThis[ENV_SOURCE_TAG];
|
|
@@ -121,3 +126,103 @@ function getEnvSource(key) {
|
|
|
121
126
|
function __resetEnvSources() {
|
|
122
127
|
getMap().clear();
|
|
123
128
|
}
|
|
129
|
+
// ── Phase v4.1-mcp.2 — Multi-source env loader for `aiden mcp serve` ──
|
|
130
|
+
//
|
|
131
|
+
// When Claude Desktop / Cursor / Claude Code spawn `aiden mcp serve`
|
|
132
|
+
// over stdio, they pass an EMPTY env block by default. Without an
|
|
133
|
+
// explicit `env: {...}` per-server entry in the client config, the
|
|
134
|
+
// spawned aiden has no GROQ_API_KEY, GEMINI_API_KEY, etc. Provider-
|
|
135
|
+
// using tools (subagent_fanout, web_search, fetch_url, …) then fail
|
|
136
|
+
// with "no providers configured".
|
|
137
|
+
//
|
|
138
|
+
// Fix: at MCP serve startup, eagerly load .env files from a small
|
|
139
|
+
// list of well-known locations into process.env. Same fill-only
|
|
140
|
+
// semantics as `loadAidenEnvFile` (preset > file). Caller passes
|
|
141
|
+
// `paths.envFile` (covers `~/.aiden/.env` and Windows-equivalent).
|
|
142
|
+
// We additionally probe the install directory so a project-local
|
|
143
|
+
// `.env` in the Aiden checkout works out of the box.
|
|
144
|
+
//
|
|
145
|
+
// NEVER log the values — only the source path and the set of keys
|
|
146
|
+
// (names) detected.
|
|
147
|
+
/** Walk up from `from` looking for an Aiden install root — a directory
|
|
148
|
+
* containing a `package.json` whose `name` is `aiden-runtime`. Returns
|
|
149
|
+
* null when no candidate is found within `maxDepth` levels. */
|
|
150
|
+
function resolveAidenInstallDir(from = __dirname, maxDepth = 8) {
|
|
151
|
+
let dir = path.resolve(from);
|
|
152
|
+
for (let i = 0; i < maxDepth; i += 1) {
|
|
153
|
+
const pkg = path.join(dir, 'package.json');
|
|
154
|
+
try {
|
|
155
|
+
const text = fs.readFileSync(pkg, 'utf8');
|
|
156
|
+
const parsed = JSON.parse(text);
|
|
157
|
+
if (parsed.name === 'aiden-runtime')
|
|
158
|
+
return dir;
|
|
159
|
+
}
|
|
160
|
+
catch { /* not present or unparseable, walk up */ }
|
|
161
|
+
const parent = path.dirname(dir);
|
|
162
|
+
if (parent === dir)
|
|
163
|
+
break;
|
|
164
|
+
dir = parent;
|
|
165
|
+
}
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
/** Load `.env` files for `aiden mcp serve`. Order:
|
|
169
|
+
*
|
|
170
|
+
* 1. `<aiden_install_dir>/.env` — project-local, dev convenience
|
|
171
|
+
* 2. `aidenHomeEnv` — per-user, `paths.envFile`
|
|
172
|
+
*
|
|
173
|
+
* 3. process.env — already loaded; takes precedence over both
|
|
174
|
+
* via fill-only semantics.
|
|
175
|
+
*
|
|
176
|
+
* Caller logs the report via the mcp-stdio logger (stderr-safe).
|
|
177
|
+
* Values are NEVER returned — only counts + key names. */
|
|
178
|
+
function loadMcpEnvSources(opts) {
|
|
179
|
+
const attempts = [];
|
|
180
|
+
const installDir = opts.installDir ?? resolveAidenInstallDir();
|
|
181
|
+
const candidates = [];
|
|
182
|
+
if (installDir)
|
|
183
|
+
candidates.push(path.join(installDir, '.env'));
|
|
184
|
+
candidates.push(opts.aidenHomeEnv);
|
|
185
|
+
let appliedTotal = 0;
|
|
186
|
+
for (const file of candidates) {
|
|
187
|
+
const before = new Set(Object.keys(process.env));
|
|
188
|
+
let exists = false;
|
|
189
|
+
try {
|
|
190
|
+
fs.accessSync(file, fs.constants.R_OK);
|
|
191
|
+
exists = true;
|
|
192
|
+
}
|
|
193
|
+
catch { /* missing — record and skip */ }
|
|
194
|
+
if (exists)
|
|
195
|
+
loadAidenEnvFile(file);
|
|
196
|
+
const appliedKeys = [];
|
|
197
|
+
for (const k of Object.keys(process.env)) {
|
|
198
|
+
if (!before.has(k))
|
|
199
|
+
appliedKeys.push(k);
|
|
200
|
+
}
|
|
201
|
+
appliedTotal += appliedKeys.length;
|
|
202
|
+
attempts.push({ path: file, exists, appliedKeys });
|
|
203
|
+
}
|
|
204
|
+
return { attempts, appliedTotal };
|
|
205
|
+
}
|
|
206
|
+
/** The provider-key surface aiden cares about for `mcp status` output.
|
|
207
|
+
* Listed explicitly so we never accidentally enumerate / log a
|
|
208
|
+
* newly-added secret. */
|
|
209
|
+
exports.KNOWN_PROVIDER_KEYS = [
|
|
210
|
+
'GROQ_API_KEY',
|
|
211
|
+
'GEMINI_API_KEY',
|
|
212
|
+
'TOGETHER_API_KEY',
|
|
213
|
+
'OPENROUTER_API_KEY',
|
|
214
|
+
'ANTHROPIC_API_KEY',
|
|
215
|
+
'OPENAI_API_KEY',
|
|
216
|
+
'CEREBRAS_API_KEY',
|
|
217
|
+
'NVIDIA_API_KEY',
|
|
218
|
+
'COHERE_API_KEY',
|
|
219
|
+
];
|
|
220
|
+
/** Snapshot of provider-key presence + source. Values NEVER returned;
|
|
221
|
+
* only the source tag (`preset`/`aiden-env`/`unset`). */
|
|
222
|
+
function describeProviderKeys(keys = exports.KNOWN_PROVIDER_KEYS) {
|
|
223
|
+
return keys.map((key) => ({
|
|
224
|
+
key,
|
|
225
|
+
present: typeof process.env[key] === 'string' && process.env[key].length > 0,
|
|
226
|
+
source: getEnvSource(key),
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2026 Shiva Deore (Taracod).
|
|
4
|
+
* Licensed under AGPL-3.0. See LICENSE for details.
|
|
5
|
+
*
|
|
6
|
+
* Aiden — local-first agent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* cli/v4/ghostMatch.ts — Tier-3.1.1 (v4.1-tier3.1.1)
|
|
10
|
+
*
|
|
11
|
+
* Compute the best ghost-text match for what the user has typed so far.
|
|
12
|
+
* The aidenPrompt component calls `findGhost(typed, ctx)` on every
|
|
13
|
+
* keystroke and renders the returned suggestion (in dim) past the
|
|
14
|
+
* cursor.
|
|
15
|
+
*
|
|
16
|
+
* Two modes:
|
|
17
|
+
*
|
|
18
|
+
* 1. Slash mode (typed starts with `/`): match against registered
|
|
19
|
+
* slash command names + aliases. Longest start-with match wins
|
|
20
|
+
* (so `/p` favours `/plugins` over `/personality`/`/providers`
|
|
21
|
+
* only if no shorter unique match exists; ties broken by
|
|
22
|
+
* alphabetical order so the result is deterministic).
|
|
23
|
+
*
|
|
24
|
+
* 2. Free-text mode: match against recent user prompts (most recent
|
|
25
|
+
* first). Returns the first prompt that starts with `typed` and
|
|
26
|
+
* is strictly longer.
|
|
27
|
+
*
|
|
28
|
+
* Returns the SUFFIX to append after the typed text, or `null` if no
|
|
29
|
+
* match exists. Empty/whitespace-only typed text → null. Typed text
|
|
30
|
+
* containing a paste-compression label (`[paste #N: …]`) → null
|
|
31
|
+
* (don't suggest over a compressed paste).
|
|
32
|
+
*/
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.findGhost = findGhost;
|
|
35
|
+
const PASTE_LABEL_RE = /\[paste #\d+:[^\]]*\]/;
|
|
36
|
+
/**
|
|
37
|
+
* Return the suffix to append (everything past `typed`) or null.
|
|
38
|
+
*
|
|
39
|
+
* Examples:
|
|
40
|
+
* findGhost('/cr', { slashNames: ['cron','clear'], … }) → 'on'
|
|
41
|
+
* findGhost('/x', { slashNames: ['cron'], … }) → null
|
|
42
|
+
* findGhost('how ', { history: ['how do I quit'], … }) → 'do I quit'
|
|
43
|
+
*/
|
|
44
|
+
function findGhost(typed, ctx) {
|
|
45
|
+
if (!typed || typed.trim().length === 0)
|
|
46
|
+
return null;
|
|
47
|
+
if (PASTE_LABEL_RE.test(typed))
|
|
48
|
+
return null;
|
|
49
|
+
if (typed.startsWith('/')) {
|
|
50
|
+
const stem = typed.slice(1);
|
|
51
|
+
if (stem.length === 0)
|
|
52
|
+
return null;
|
|
53
|
+
const all = [...ctx.slashNames, ...ctx.slashAliases];
|
|
54
|
+
// Longest start-with match wins. Ties broken alphabetically.
|
|
55
|
+
const candidates = all
|
|
56
|
+
.filter((n) => n.startsWith(stem) && n.length > stem.length)
|
|
57
|
+
.sort();
|
|
58
|
+
if (candidates.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
// Prefer the SHORTEST candidate that uniquely starts with stem
|
|
61
|
+
// (more likely the user-intended completion). Fall back to the
|
|
62
|
+
// alphabetically-first if all share the same length.
|
|
63
|
+
const shortest = candidates.reduce((best, c) => c.length < best.length ? c : best);
|
|
64
|
+
return shortest.slice(stem.length);
|
|
65
|
+
}
|
|
66
|
+
// Free-text — history fallback. Prefer the most-recent strict
|
|
67
|
+
// start-with match.
|
|
68
|
+
for (const past of ctx.history) {
|
|
69
|
+
if (past.startsWith(typed) && past.length > typed.length) {
|
|
70
|
+
return past.slice(typed.length);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|