@indianic/dialout 1.10.0 → 1.12.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.
Files changed (117) hide show
  1. package/README.md +14 -0
  2. package/dist/ai-adapters/claude.d.ts.map +1 -1
  3. package/dist/ai-adapters/claude.js +18 -1
  4. package/dist/ai-adapters/claude.js.map +1 -1
  5. package/dist/ai-sessions.d.ts +26 -1
  6. package/dist/ai-sessions.d.ts.map +1 -1
  7. package/dist/ai-sessions.js +43 -7
  8. package/dist/ai-sessions.js.map +1 -1
  9. package/dist/cli-client.d.ts +60 -0
  10. package/dist/cli-client.d.ts.map +1 -0
  11. package/dist/cli-client.js +246 -0
  12. package/dist/cli-client.js.map +1 -0
  13. package/dist/cli-terminal.d.ts +101 -0
  14. package/dist/cli-terminal.d.ts.map +1 -0
  15. package/dist/cli-terminal.js +1121 -0
  16. package/dist/cli-terminal.js.map +1 -0
  17. package/dist/cli.js +84 -0
  18. package/dist/cli.js.map +1 -1
  19. package/dist/file-transfer.d.ts +18 -0
  20. package/dist/file-transfer.d.ts.map +1 -0
  21. package/dist/file-transfer.js +266 -0
  22. package/dist/file-transfer.js.map +1 -0
  23. package/dist/fs-browser.d.ts +3 -1
  24. package/dist/fs-browser.d.ts.map +1 -1
  25. package/dist/fs-browser.js +35 -6
  26. package/dist/fs-browser.js.map +1 -1
  27. package/dist/mcp-stdio.d.ts +10 -0
  28. package/dist/mcp-stdio.d.ts.map +1 -0
  29. package/dist/mcp-stdio.js +199 -0
  30. package/dist/mcp-stdio.js.map +1 -0
  31. package/dist/shared-shell.d.ts +77 -0
  32. package/dist/shared-shell.d.ts.map +1 -0
  33. package/dist/shared-shell.js +282 -0
  34. package/dist/shared-shell.js.map +1 -0
  35. package/dist/websocket.d.ts.map +1 -1
  36. package/dist/websocket.js +413 -1
  37. package/dist/websocket.js.map +1 -1
  38. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts +2 -0
  39. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.d.ts.map +1 -0
  40. package/node_modules/@indianic/dialout-shared/dist/ai-chat-prefs.test.js +38 -0
  41. package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts +2 -0
  42. package/node_modules/@indianic/dialout-shared/dist/attachments.test.d.ts.map +1 -0
  43. package/node_modules/@indianic/dialout-shared/dist/attachments.test.js +31 -0
  44. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts +27 -0
  45. package/node_modules/@indianic/dialout-shared/dist/captured.d.ts.map +1 -0
  46. package/node_modules/@indianic/dialout-shared/dist/captured.js +23 -0
  47. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts +2 -0
  48. package/node_modules/@indianic/dialout-shared/dist/captured.test.d.ts.map +1 -0
  49. package/node_modules/@indianic/dialout-shared/dist/captured.test.js +27 -0
  50. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts +6 -0
  51. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.d.ts.map +1 -1
  52. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.js +15 -4
  53. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts +2 -0
  54. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.d.ts.map +1 -0
  55. package/node_modules/@indianic/dialout-shared/dist/chat-blocks.test.js +81 -0
  56. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts +2 -0
  57. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.d.ts.map +1 -0
  58. package/node_modules/@indianic/dialout-shared/dist/command-filter.test.js +63 -0
  59. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts +2 -0
  60. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.d.ts.map +1 -0
  61. package/node_modules/@indianic/dialout-shared/dist/composer-behaviour.test.js +27 -0
  62. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts +7 -0
  63. package/node_modules/@indianic/dialout-shared/dist/event-merge.d.ts.map +1 -1
  64. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts +2 -0
  65. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.d.ts.map +1 -0
  66. package/node_modules/@indianic/dialout-shared/dist/event-merge.test.js +66 -0
  67. package/node_modules/@indianic/dialout-shared/dist/events.d.ts +1 -0
  68. package/node_modules/@indianic/dialout-shared/dist/events.d.ts.map +1 -1
  69. package/node_modules/@indianic/dialout-shared/dist/index.d.ts +6 -1
  70. package/node_modules/@indianic/dialout-shared/dist/index.d.ts.map +1 -1
  71. package/node_modules/@indianic/dialout-shared/dist/index.js +15 -1
  72. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts +37 -0
  73. package/node_modules/@indianic/dialout-shared/dist/notices.d.ts.map +1 -0
  74. package/node_modules/@indianic/dialout-shared/dist/notices.js +124 -0
  75. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts +2 -0
  76. package/node_modules/@indianic/dialout-shared/dist/notices.test.d.ts.map +1 -0
  77. package/node_modules/@indianic/dialout-shared/dist/notices.test.js +92 -0
  78. package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts +2 -0
  79. package/node_modules/@indianic/dialout-shared/dist/notify.test.d.ts.map +1 -0
  80. package/node_modules/@indianic/dialout-shared/dist/notify.test.js +51 -0
  81. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts +17 -0
  82. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.d.ts.map +1 -1
  83. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.js +27 -0
  84. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts +2 -0
  85. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.d.ts.map +1 -0
  86. package/node_modules/@indianic/dialout-shared/dist/pane-prompt.test.js +184 -0
  87. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts +2 -0
  88. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.d.ts.map +1 -0
  89. package/node_modules/@indianic/dialout-shared/dist/scroll-pin.test.js +21 -0
  90. package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts +9 -0
  91. package/node_modules/@indianic/dialout-shared/dist/thinking.d.ts.map +1 -0
  92. package/node_modules/@indianic/dialout-shared/dist/thinking.js +38 -0
  93. package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts +2 -0
  94. package/node_modules/@indianic/dialout-shared/dist/thinking.test.d.ts.map +1 -0
  95. package/node_modules/@indianic/dialout-shared/dist/thinking.test.js +30 -0
  96. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts +2 -0
  97. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.d.ts.map +1 -0
  98. package/node_modules/@indianic/dialout-shared/dist/tmux-prefs.test.js +95 -0
  99. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts +2 -0
  100. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.d.ts.map +1 -0
  101. package/node_modules/@indianic/dialout-shared/dist/tool-appearance.test.js +38 -0
  102. package/node_modules/@indianic/dialout-shared/package.json +2 -2
  103. package/node_modules/@indianic/dialout-shared/src/captured.test.ts +29 -0
  104. package/node_modules/@indianic/dialout-shared/src/captured.ts +46 -0
  105. package/node_modules/@indianic/dialout-shared/src/chat-blocks.test.ts +14 -0
  106. package/node_modules/@indianic/dialout-shared/src/chat-blocks.ts +14 -5
  107. package/node_modules/@indianic/dialout-shared/src/event-merge.test.ts +27 -0
  108. package/node_modules/@indianic/dialout-shared/src/event-merge.ts +11 -0
  109. package/node_modules/@indianic/dialout-shared/src/events.ts +5 -1
  110. package/node_modules/@indianic/dialout-shared/src/index.ts +8 -1
  111. package/node_modules/@indianic/dialout-shared/src/notices.test.ts +102 -0
  112. package/node_modules/@indianic/dialout-shared/src/notices.ts +159 -0
  113. package/node_modules/@indianic/dialout-shared/src/pane-prompt.test.ts +23 -1
  114. package/node_modules/@indianic/dialout-shared/src/pane-prompt.ts +27 -0
  115. package/node_modules/@indianic/dialout-shared/src/thinking.test.ts +32 -0
  116. package/node_modules/@indianic/dialout-shared/src/thinking.ts +32 -0
  117. package/package.json +1 -1
@@ -0,0 +1,159 @@
1
+ import { thinkingDuration } from './thinking';
2
+
3
+ // Not every "user" message in a transcript came from a person.
4
+ //
5
+ // The CLIs inject their own machinery into the user role: a background task
6
+ // finishing, a subagent handing its report back, a slash command, the output
7
+ // of a local command, a reminder the harness wants the model to re-read. Each
8
+ // arrives as an XML-ish frame, and rendering those frames verbatim is what
9
+ // filled the chat with `<task-notification>` and `<tool-use-id>` instead of
10
+ // the one line the terminal prints for the same event.
11
+ //
12
+ // So a user message is read before it is rendered. It comes back as the
13
+ // person's words, as a one-line notice with the detail folded away, or as
14
+ // nothing at all — the terminal shows no trace of a system reminder and
15
+ // neither should we. The body kept on a notice is markdown, dedented, ready
16
+ // for the same renderer that draws an agent's reply.
17
+
18
+ export type NoticeKind = 'task' | 'handback' | 'session' | 'command' | 'output' | 'compact';
19
+
20
+ export interface MessageNotice {
21
+ kind: NoticeKind;
22
+ /** The single line, the way the terminal would print it. */
23
+ label: string;
24
+ /** What the line earns a suffix for: a duration, a tool count, an id. */
25
+ detail?: string;
26
+ /** Markdown worth keeping behind a toggle; absent means nothing to open. */
27
+ body?: string;
28
+ }
29
+
30
+ export type ReadMessage =
31
+ | { kind: 'text'; text: string }
32
+ | { kind: 'notice'; notice: MessageNotice }
33
+ | { kind: 'hidden' };
34
+
35
+ /** Reminders are a frame around other content as often as they are content. */
36
+ const REMINDER = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
37
+ const CAVEAT = /<local-command-caveat>[\s\S]*?<\/local-command-caveat>/g;
38
+ const HANDBACK_FRAME = 'The report follows:';
39
+
40
+ function tag(src: string, name: string): string {
41
+ const m = new RegExp(`<${name}>([\\s\\S]*?)</${name}>`).exec(src);
42
+ return m ? m[1].trim() : '';
43
+ }
44
+
45
+ /**
46
+ * Strip the indent a frame added, so the markdown inside survives.
47
+ *
48
+ * The hand-back frame indents every line of a subagent's report by two
49
+ * spaces — deliberately, so a forged frame line at column zero is visible.
50
+ * Left in place that indent turns the report's `- ` bullets into continuation
51
+ * lines and its fenced blocks into something the parser never reaches.
52
+ */
53
+ export function dedent(src: string): string {
54
+ const lines = String(src || '').replace(/\r\n?/g, '\n').split('\n')
55
+ .map((l) => l.replace(/\t/g, ' '));
56
+ let min = Infinity;
57
+ for (const l of lines) {
58
+ if (!l.trim()) continue;
59
+ min = Math.min(min, (/^ */.exec(l) as RegExpExecArray)[0].length);
60
+ }
61
+ if (!isFinite(min) || min === 0) return lines.join('\n').trim();
62
+ return lines.map((l) => (l.trim() ? l.slice(min) : '')).join('\n').trim();
63
+ }
64
+
65
+ function taskNotice(src: string): MessageNotice {
66
+ const n = src.slice(src.indexOf('<task-notification>'));
67
+ const summary = tag(n, 'summary');
68
+ const status = tag(n, 'status');
69
+ const ms = Number(tag(n, 'duration_ms') || 0);
70
+ const tools = Number(tag(n, 'tool_uses') || 0);
71
+ const bits: string[] = [];
72
+ if (ms > 0) bits.push(thinkingDuration(ms));
73
+ if (tools > 0) bits.push(`${tools} tool${tools === 1 ? '' : 's'}`);
74
+ // The result is usually a pointer to the report rather than the report, and
75
+ // the note explains the lifecycle. Both are worth keeping, neither is worth
76
+ // a line of its own.
77
+ const body = [tag(n, 'result'), tag(n, 'note')].filter(Boolean).join('\n\n');
78
+ return {
79
+ kind: 'task',
80
+ label: summary || (status ? `Background task ${status}` : 'Background task'),
81
+ detail: bits.join(' · ') || undefined,
82
+ body: body || undefined,
83
+ };
84
+ }
85
+
86
+ function handbackNotice(src: string, from: string): MessageNotice {
87
+ const open = /<agent-message from="([^"]*)">/.exec(src);
88
+ let body = open ? src.slice((open.index ?? 0) + open[0].length) : src;
89
+ body = body.replace(/<\/agent-message>\s*$/, '');
90
+ const handback = body.includes('[Subagent hand-back]');
91
+ // Everything before "The report follows:" is the harness explaining that
92
+ // the report carries no user authority. True, and already enforced by the
93
+ // fact that we render it as a notice rather than as a request.
94
+ const at = body.indexOf(HANDBACK_FRAME);
95
+ if (at >= 0) body = body.slice(at + HANDBACK_FRAME.length);
96
+ const who = (from || '').trim();
97
+ return {
98
+ kind: handback ? 'handback' : 'session',
99
+ label: handback ? 'Report from a delegated agent' : 'Message from another session',
100
+ detail: who || undefined,
101
+ body: dedent(body) || undefined,
102
+ };
103
+ }
104
+
105
+ /**
106
+ * What to draw for one user-role message.
107
+ *
108
+ * Order matters: a task notification usually arrives wrapped in a reminder,
109
+ * and a slash command carries its own stdout, so the frames are peeled before
110
+ * anything is matched and the more specific shape is tested first.
111
+ */
112
+ export function readUserMessage(raw: string): ReadMessage {
113
+ const full = String(raw || '').replace(/\r\n?/g, '\n');
114
+ const src = full.replace(REMINDER, '\n').replace(CAVEAT, '\n').trim();
115
+ if (!src) return { kind: 'hidden' };
116
+
117
+ if (src.includes('<task-notification>')) {
118
+ return { kind: 'notice', notice: taskNotice(src) };
119
+ }
120
+
121
+ const from = /<agent-message from="([^"]*)">/.exec(src);
122
+ if (from || src.startsWith('Another Claude session sent a message')) {
123
+ return { kind: 'notice', notice: handbackNotice(src, from ? from[1] : '') };
124
+ }
125
+
126
+ if (src.includes('<command-name>')) {
127
+ const name = tag(src, 'command-name');
128
+ const args = tag(src, 'command-args');
129
+ const out = tag(src, 'local-command-stdout');
130
+ return {
131
+ kind: 'notice',
132
+ notice: {
133
+ kind: 'command',
134
+ label: [name || 'Command', args].filter(Boolean).join(' '),
135
+ body: out || undefined,
136
+ },
137
+ };
138
+ }
139
+
140
+ if (src.includes('<local-command-stdout>')) {
141
+ const out = tag(src, 'local-command-stdout');
142
+ return { kind: 'notice', notice: { kind: 'output', label: 'Command output', body: out || undefined } };
143
+ }
144
+
145
+ if (src.includes('<tools-used-before-summary>')
146
+ || src.startsWith('This session is being continued from a previous conversation')) {
147
+ return {
148
+ kind: 'notice',
149
+ notice: { kind: 'compact', label: 'Earlier conversation summarised', body: src },
150
+ };
151
+ }
152
+
153
+ // A frame we do not know, with nothing but tags inside it, is machinery too.
154
+ if (/^<[a-z][\w-]*>/.test(src) && !/[^\s<>/\w-]/.test(src.replace(/<[^>]*>/g, ''))) {
155
+ return { kind: 'hidden' };
156
+ }
157
+
158
+ return { kind: 'text', text: src };
159
+ }
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { detectPanePrompt, isSafeKeyList, promptKeys } from './pane-prompt';
2
+ import { detectPanePrompt, freeTextRow, isFreeTextOption, isSafeKeyList, promptKeys, promptMoveKeys } from './pane-prompt';
3
3
 
4
4
  // Shapes as the CLIs draw them (spacing kept), plus the noise around them.
5
5
  const CLAUDE_BASH = `
@@ -177,3 +177,25 @@ describe('promptKeys', () => {
177
177
  expect(isSafeKeyList('Enter')).toBe(false);
178
178
  });
179
179
  });
180
+
181
+ describe('free-text rows', () => {
182
+ it('knows which rows want typing, and moves there without choosing', () => {
183
+ expect(isFreeTextOption('Type something.')).toBe(true);
184
+ expect(isFreeTextOption('Other')).toBe(true);
185
+ expect(isFreeTextOption('No, and tell Claude what to do differently (esc)')).toBe(false);
186
+ expect(isFreeTextOption('Yes')).toBe(false);
187
+ const p = detectPanePrompt(CLAUDE_QUESTION)!;
188
+ expect(promptMoveKeys(p, 3)).toEqual(['Down', 'Down']);
189
+ expect(promptMoveKeys(p, 1)).toEqual([]);
190
+ });
191
+
192
+ it('finds the row that wants typing, and stops once it holds text', () => {
193
+ const p = detectPanePrompt(CLAUDE_QUESTION)!;
194
+ expect(freeTextRow(p)).toBe(3);
195
+ // The TUI echoes what is typed in place of the label; the row is then
196
+ // mid-answer, not an empty invitation, so the card must not reopen on it.
197
+ const typed = detectPanePrompt(CLAUDE_QUESTION.replace('3. Type something.', '3. Teal like the sea'))!;
198
+ expect(freeTextRow(typed)).toBe(0);
199
+ expect(freeTextRow(detectPanePrompt(CLAUDE_BASH)!)).toBe(0);
200
+ });
201
+ });
@@ -216,3 +216,30 @@ export function promptKeys(p: PanePrompt, n: number): string[] {
216
216
  const move = to > from ? Array(to - from).fill('Down') : Array(from - to).fill('Up');
217
217
  return [...move, 'Enter'];
218
218
  }
219
+
220
+ /**
221
+ * A row that takes typed text rather than being an answer itself: Claude
222
+ * Code's "Type something." / an "Other" row. Picking it with Enter answers
223
+ * with nothing, so the card asks for the text first.
224
+ */
225
+ export function isFreeTextOption(label: string): boolean {
226
+ return /^(type something|type your own|other|something else)\.?(\s*\(.*\))?$/i.test(label.trim());
227
+ }
228
+
229
+ /**
230
+ * Keys that put the cursor on row `n` without choosing it. For a free-text
231
+ * row: move there, then the text is typed and Enter pressed (as one message).
232
+ */
233
+ export function promptMoveKeys(p: PanePrompt, n: number): string[] {
234
+ return promptKeys(p, n).slice(0, -1);
235
+ }
236
+
237
+ /**
238
+ * The free-text row's number, or 0 if the menu has none. A TUI echoes what is
239
+ * typed in place of that row's label, so a row that already holds text no
240
+ * longer matches — which is correct: it is mid-answer, not an empty invitation.
241
+ */
242
+ export function freeTextRow(p: PanePrompt): number {
243
+ const row = p.options.find((o) => o.checked === null && isFreeTextOption(o.label));
244
+ return row ? row.n : 0;
245
+ }
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { thinkingDuration, thinkingLabel } from './thinking';
3
+
4
+ describe('thinkingDuration', () => {
5
+ it('reads like a stopwatch at every scale', () => {
6
+ expect(thinkingDuration(490)).toBe('490ms');
7
+ expect(thinkingDuration(1422)).toBe('1.4s');
8
+ expect(thinkingDuration(12252)).toBe('12s');
9
+ expect(thinkingDuration(59_400)).toBe('59s');
10
+ expect(thinkingDuration(60_000)).toBe('1m');
11
+ expect(thinkingDuration(125_000)).toBe('2m 5s');
12
+ });
13
+
14
+ it('says nothing rather than "0s" when there is no duration to report', () => {
15
+ expect(thinkingDuration(0)).toBe('');
16
+ expect(thinkingDuration(-5)).toBe('');
17
+ expect(thinkingDuration(NaN)).toBe('');
18
+ expect(thinkingDuration(Infinity)).toBe('');
19
+ });
20
+ });
21
+
22
+ describe('thinkingLabel', () => {
23
+ it('names the duration when the vendor gave one', () => {
24
+ expect(thinkingLabel(1422)).toBe('Thought for 1.4s');
25
+ expect(thinkingLabel(12252)).toBe('Thought for 12s');
26
+ });
27
+
28
+ it('falls back to the vague wording, never a bare "thinking…"', () => {
29
+ expect(thinkingLabel(undefined)).toBe('Thought for a moment');
30
+ expect(thinkingLabel(0)).toBe('Thought for a moment');
31
+ });
32
+ });
@@ -0,0 +1,32 @@
1
+ // How a spell of reasoning is labelled in the transcript.
2
+ //
3
+ // Most of it never reaches us. Claude Code runs adaptive thinking with a
4
+ // `clear_thinking_20251015` context edit, which strips the words out of the
5
+ // persisted history and leaves a signed, empty block behind — measured at 515
6
+ // of 615 blocks in one real session. Codex and Grok encrypt theirs outright.
7
+ // So a thinking row usually has no words to show, only the fact that it
8
+ // happened and `thinkingDurationMs` saying how long for. That is worth
9
+ // showing: it is the difference between an agent that is stuck and one that is
10
+ // thinking hard.
11
+
12
+ /** "1.4s", "12s", "2m 5s" — the duration Claude Code reports for a turn. */
13
+ export function thinkingDuration(ms: number): string {
14
+ if (!Number.isFinite(ms) || ms <= 0) return '';
15
+ if (ms < 1000) return `${Math.round(ms)}ms`;
16
+ const s = ms / 1000;
17
+ if (s < 10) return `${s.toFixed(1)}s`;
18
+ if (s < 60) return `${Math.round(s)}s`;
19
+ const m = Math.floor(s / 60);
20
+ const rest = Math.round(s % 60);
21
+ return rest ? `${m}m ${rest}s` : `${m}m`;
22
+ }
23
+
24
+ /**
25
+ * The label for a thinking row. "Thought for 1.4s" when the vendor said how
26
+ * long, and the vaguer wording only when it did not — never a bare
27
+ * "thinking…", which read as though something were still in flight.
28
+ */
29
+ export function thinkingLabel(ms?: number): string {
30
+ const d = ms == null ? '' : thinkingDuration(ms);
31
+ return d ? `Thought for ${d}` : 'Thought for a moment';
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@indianic/dialout",
3
- "version": "1.10.0",
3
+ "version": "1.12.0",
4
4
  "description": "Dialout agent — reach your office, home or build machine from any browser or phone. Remote tmux terminals, live port checks, filesystem browsing, process control and HTTP tunnels. The agent dials out: no inbound ports, no VPN, no port forwarding.",
5
5
  "bin": {
6
6
  "dialout": "./bin/devdash-agent.js"