@indianic/dialout 1.13.2 → 1.14.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/dist/ai-adapters/augment.d.ts +3 -0
- package/dist/ai-adapters/augment.d.ts.map +1 -0
- package/dist/ai-adapters/augment.js +147 -0
- package/dist/ai-adapters/augment.js.map +1 -0
- package/dist/ai-adapters/index.d.ts.map +1 -1
- package/dist/ai-adapters/index.js +2 -0
- package/dist/ai-adapters/index.js.map +1 -1
- package/dist/ai-json-tail.d.ts +26 -0
- package/dist/ai-json-tail.d.ts.map +1 -0
- package/dist/ai-json-tail.js +153 -0
- package/dist/ai-json-tail.js.map +1 -0
- package/dist/ai-session-detector.d.ts +22 -4
- package/dist/ai-session-detector.d.ts.map +1 -1
- package/dist/ai-session-detector.js +43 -6
- package/dist/ai-session-detector.js.map +1 -1
- package/dist/ai-sessions.d.ts +4 -2
- package/dist/ai-sessions.d.ts.map +1 -1
- package/dist/ai-sessions.js +24 -8
- package/dist/ai-sessions.js.map +1 -1
- package/dist/ai-transcript-locator.d.ts +74 -0
- package/dist/ai-transcript-locator.d.ts.map +1 -1
- package/dist/ai-transcript-locator.js +181 -0
- package/dist/ai-transcript-locator.js.map +1 -1
- package/dist/ai-transcript-tail.d.ts +17 -1
- package/dist/ai-transcript-tail.d.ts.map +1 -1
- package/dist/ai-transcript-tail.js.map +1 -1
- package/dist/cli-client.d.ts.map +1 -1
- package/dist/cli-client.js +1 -0
- package/dist/cli-client.js.map +1 -1
- package/dist/cli.js +96 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +21 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +28 -2
- package/dist/config.js.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/custom-agents.d.ts +97 -0
- package/node_modules/@indianic/dialout-shared/dist/custom-agents.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/custom-agents.js +147 -0
- package/node_modules/@indianic/dialout-shared/dist/custom-agents.test.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/custom-agents.test.d.ts.map +1 -0
- package/node_modules/@indianic/dialout-shared/dist/custom-agents.test.js +155 -0
- package/node_modules/@indianic/dialout-shared/dist/index.d.ts +2 -0
- package/node_modules/@indianic/dialout-shared/dist/index.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/dist/index.js +11 -2
- package/node_modules/@indianic/dialout-shared/dist/kinds.d.ts +1 -1
- package/node_modules/@indianic/dialout-shared/dist/kinds.d.ts.map +1 -1
- package/node_modules/@indianic/dialout-shared/package.json +1 -1
- package/node_modules/@indianic/dialout-shared/src/.impeccable/hook.cache.json +1 -0
- package/node_modules/@indianic/dialout-shared/src/.omc/state/sessions/e864cc5c-dad7-49c4-8666-6d7fb2297d2a/pre-tool-advisory-throttle.json +14 -0
- package/node_modules/@indianic/dialout-shared/src/custom-agents.test.ts +185 -0
- package/node_modules/@indianic/dialout-shared/src/custom-agents.ts +198 -0
- package/node_modules/@indianic/dialout-shared/src/index.ts +5 -0
- package/node_modules/@indianic/dialout-shared/src/kinds.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
parseCustomAgent,
|
|
4
|
+
parseCustomAgents,
|
|
5
|
+
customAgentRule,
|
|
6
|
+
expandTranscript,
|
|
7
|
+
transcriptMatcher,
|
|
8
|
+
isAiKind,
|
|
9
|
+
} from './custom-agents';
|
|
10
|
+
|
|
11
|
+
const good = {
|
|
12
|
+
name: 'sagent',
|
|
13
|
+
kind: 'claude' as const,
|
|
14
|
+
transcript: '{home}/.myagent/sessions/*.jsonl',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('parseCustomAgent', () => {
|
|
18
|
+
it('accepts a well-formed declaration', () => {
|
|
19
|
+
const parsed = parseCustomAgent(good);
|
|
20
|
+
expect(parsed).toEqual({ ok: true, agent: good });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('trims and lowercases the kind, so "Claude" is not a mystery failure', () => {
|
|
24
|
+
const parsed = parseCustomAgent({ ...good, kind: ' Claude ' });
|
|
25
|
+
expect(parsed.ok && parsed.agent.kind).toBe('claude');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('rejects a kind with no adapter behind it', () => {
|
|
29
|
+
// The whole point: `kind` chooses which parser runs. A kind we have no
|
|
30
|
+
// adapter for would produce a session with no conversation in it.
|
|
31
|
+
const parsed = parseCustomAgent({ ...good, kind: 'augment-next' });
|
|
32
|
+
expect(parsed.ok).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('the path rules, which are the security boundary', () => {
|
|
36
|
+
// The agent runs as the user and streams whatever it is pointed at into a
|
|
37
|
+
// chat. These rules are what stop a declaration being a way to read
|
|
38
|
+
// arbitrary files.
|
|
39
|
+
|
|
40
|
+
it('requires the path to start at {home}', () => {
|
|
41
|
+
for (const transcript of ['/etc/passwd', '~/.ssh/id_rsa', '/{home}/x.jsonl', 'relative/x.jsonl']) {
|
|
42
|
+
const parsed = parseCustomAgent({ ...good, transcript });
|
|
43
|
+
expect(parsed.ok, transcript).toBe(false);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('rejects traversal outright', () => {
|
|
48
|
+
const parsed = parseCustomAgent({ ...good, transcript: '{home}/../../etc/hosts.jsonl' });
|
|
49
|
+
expect(parsed.ok).toBe(false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('requires a .json or .jsonl file, so {home}/.ssh/* is not declarable', () => {
|
|
53
|
+
const parsed = parseCustomAgent({ ...good, transcript: '{home}/.ssh/*' });
|
|
54
|
+
expect(parsed.ok).toBe(false);
|
|
55
|
+
expect(parseCustomAgent({ ...good, transcript: '{home}/.x/a.json' }).ok).toBe(true);
|
|
56
|
+
expect(parseCustomAgent({ ...good, transcript: '{home}/.x/a.jsonl' }).ok).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('allows a glob only in the file name', () => {
|
|
60
|
+
// A '*' in a directory segment would walk the home directory.
|
|
61
|
+
expect(parseCustomAgent({ ...good, transcript: '{home}/*/sessions/a.jsonl' }).ok).toBe(false);
|
|
62
|
+
expect(parseCustomAgent({ ...good, transcript: '{home}/.x/*.jsonl' }).ok).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('allows at most one glob', () => {
|
|
66
|
+
expect(parseCustomAgent({ ...good, transcript: '{home}/.x/*-*.jsonl' }).ok).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('reports an unknown placeholder instead of looking up a literal directory', () => {
|
|
70
|
+
const parsed = parseCustomAgent({ ...good, transcript: '{home}/.x/{sessionid}/a.jsonl' });
|
|
71
|
+
expect(parsed.ok).toBe(false);
|
|
72
|
+
expect(parsed.ok === false && parsed.error).toContain('{sessionid}');
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('the name rules', () => {
|
|
77
|
+
it('rejects a name that would match half the process table', () => {
|
|
78
|
+
for (const name of ['', ' ', '*', '.*', 'my agent', 'bin/agent', 'a'.repeat(65)]) {
|
|
79
|
+
expect(parseCustomAgent({ ...good, name }).ok, JSON.stringify(name)).toBe(false);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('accepts the characters real binaries use', () => {
|
|
84
|
+
for (const name of ['agy', 'my-agent', 'my_agent', 'agent.sh', 'claude2', 'c++agent']) {
|
|
85
|
+
expect(parseCustomAgent({ ...good, name }).ok, name).toBe(true);
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('rejects junk rather than throwing', () => {
|
|
91
|
+
for (const bad of [null, undefined, 42, 'str', [], {}]) {
|
|
92
|
+
expect(parseCustomAgent(bad).ok).toBe(false);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe('parseCustomAgents', () => {
|
|
98
|
+
it('treats absent as none', () => {
|
|
99
|
+
expect(parseCustomAgents(undefined)).toEqual({ ok: true, agents: [] });
|
|
100
|
+
expect(parseCustomAgents(null)).toEqual({ ok: true, agents: [] });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('reports the first bad entry rather than silently dropping it', () => {
|
|
104
|
+
// Silently dropping would leave the user with a config they can see and a
|
|
105
|
+
// session they cannot, and nothing connecting the two.
|
|
106
|
+
const parsed = parseCustomAgents([good, { ...good, name: 'x', kind: 'nope' }]);
|
|
107
|
+
expect(parsed.ok).toBe(false);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('rejects two entries claiming one name', () => {
|
|
111
|
+
const parsed = parseCustomAgents([good, { ...good, transcript: '{home}/.other/a.json' }]);
|
|
112
|
+
expect(parsed.ok).toBe(false);
|
|
113
|
+
expect(parsed.ok === false && parsed.error).toContain('duplicate');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('rejects a non-array', () => {
|
|
117
|
+
expect(parseCustomAgents({ name: 'x' }).ok).toBe(false);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe('customAgentRule', () => {
|
|
122
|
+
const rule = (name: string) => customAgentRule({ ...good, name });
|
|
123
|
+
|
|
124
|
+
it('anchors on a path-segment boundary, like the built-in rules', () => {
|
|
125
|
+
expect(rule('sagent').test('sagent')).toBe(true);
|
|
126
|
+
expect(rule('sagent').test('sagent --resume')).toBe(true);
|
|
127
|
+
expect(rule('sagent').test('/usr/local/bin/sagent')).toBe(true);
|
|
128
|
+
// A bare space before the name is neither start-of-string nor a slash.
|
|
129
|
+
expect(rule('sagent').test('vim sagent')).toBe(false);
|
|
130
|
+
expect(rule('sagent').test('sagentry')).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('escapes the name, so a declaration cannot inject a pattern', () => {
|
|
134
|
+
// '.' must match a literal dot and nothing else; without escaping,
|
|
135
|
+
// 'a.y' would match 'agy' — and a name of '.*' would match everything.
|
|
136
|
+
expect(rule('a.y').test('a.y')).toBe(true);
|
|
137
|
+
expect(rule('a.y').test('agy')).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('expandTranscript', () => {
|
|
142
|
+
it('fills the placeholders it is given', () => {
|
|
143
|
+
expect(expandTranscript('{home}/.x/{pid}/a.jsonl', { home: '/h', pid: 42 }))
|
|
144
|
+
.toBe('/h/.x/42/a.jsonl');
|
|
145
|
+
expect(expandTranscript('{home}/.x/{tty}.json', { home: '/h', tty: '/dev/ttys1' }))
|
|
146
|
+
.toBe('/h/.x//dev/ttys1.json');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('returns null when a placeholder it needs has no value', () => {
|
|
150
|
+
// Otherwise '{cwd}' is looked up as a literal directory of that name,
|
|
151
|
+
// which never matches and reads as "your declaration is wrong".
|
|
152
|
+
expect(expandTranscript('{home}/.x/{cwd}/a.jsonl', { home: '/h' })).toBeNull();
|
|
153
|
+
expect(expandTranscript('{home}/.x/a.jsonl', { home: '' })).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('transcriptMatcher', () => {
|
|
158
|
+
it('matches an exact file name when there is no glob', () => {
|
|
159
|
+
const m = transcriptMatcher('/h/.x/chat.jsonl');
|
|
160
|
+
expect(m.dir).toBe('/h/.x');
|
|
161
|
+
expect(m.match('chat.jsonl')).toBe(true);
|
|
162
|
+
expect(m.match('other.jsonl')).toBe(false);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('expands a glob within the file name only', () => {
|
|
166
|
+
const m = transcriptMatcher('/h/.x/*.jsonl');
|
|
167
|
+
expect(m.dir).toBe('/h/.x');
|
|
168
|
+
expect(m.match('a.jsonl')).toBe(true);
|
|
169
|
+
expect(m.match('deep/a.jsonl')).toBe(false);
|
|
170
|
+
expect(m.match('a.json')).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('treats the non-glob part of the name literally', () => {
|
|
174
|
+
const m = transcriptMatcher('/h/.x/roll.out-*.jsonl');
|
|
175
|
+
expect(m.match('roll.out-1.jsonl')).toBe(true);
|
|
176
|
+
expect(m.match('rollXout-1.jsonl')).toBe(false);
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe('isAiKind', () => {
|
|
181
|
+
it('knows the four vendors with adapters', () => {
|
|
182
|
+
for (const k of ['claude', 'codex', 'grok', 'augment']) expect(isAiKind(k)).toBe(true);
|
|
183
|
+
for (const k of ['gemini', '', null, undefined, 7]) expect(isAiKind(k)).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { AiKind } from './kinds';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A coding agent this machine's owner has declared by hand.
|
|
5
|
+
*
|
|
6
|
+
* Two cases this exists for, and one it deliberately does not cover.
|
|
7
|
+
*
|
|
8
|
+
* It covers a RENAMED BINARY — a copy of a supported CLI living under another
|
|
9
|
+
* name, where argv really is `sagent` and the detector's rules cannot see it.
|
|
10
|
+
* And it covers a supported format written somewhere we do not look.
|
|
11
|
+
*
|
|
12
|
+
* It does NOT make a genuinely new vendor readable. `kind` chooses which
|
|
13
|
+
* existing parser runs, so a custom agent must write a transcript one of the
|
|
14
|
+
* adapters already understands. There is no way around that: knowing a process
|
|
15
|
+
* is an agent tells you nothing about how to read its conversation.
|
|
16
|
+
*
|
|
17
|
+
* Worth being clear about what is NOT needed here: a shell alias, function or
|
|
18
|
+
* wrapper script needs no entry at all. `sagent() { claude "$@"; }` runs a
|
|
19
|
+
* process whose argv is `claude`, and findAgentInPane walks descendants, so it
|
|
20
|
+
* is already detected. Only a renamed binary is invisible.
|
|
21
|
+
*
|
|
22
|
+
* ---
|
|
23
|
+
*
|
|
24
|
+
* MACHINE-LOCAL BY DESIGN. These live in ~/.dialout/config.json and are never
|
|
25
|
+
* accepted from the server.
|
|
26
|
+
*
|
|
27
|
+
* The agent runs as the user, with the user's full read access, and it streams
|
|
28
|
+
* whatever it is pointed at into a chat. A server-supplied path would therefore
|
|
29
|
+
* be a file-read primitive: point it at ~/.ssh/id_rsa or a .env and the
|
|
30
|
+
* contents leave the machine. Keeping the declaration on the machine means
|
|
31
|
+
* compromising a web account cannot turn every agent into a file reader — the
|
|
32
|
+
* same reasoning that keeps the ws-server bound to 127.0.0.1 and filters every
|
|
33
|
+
* value interpolated into the cowork rc block.
|
|
34
|
+
*
|
|
35
|
+
* The validation below is the second line of defence, not the first.
|
|
36
|
+
*/
|
|
37
|
+
export interface CustomAgent {
|
|
38
|
+
/** The command name as the process table shows it, e.g. 'sagent'. */
|
|
39
|
+
name: string;
|
|
40
|
+
/** Which existing adapter reads this agent's transcript. */
|
|
41
|
+
kind: AiKind;
|
|
42
|
+
/** Where the transcript lives; see expandTranscript for the placeholders. */
|
|
43
|
+
transcript: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const AI_KINDS: readonly AiKind[] = ['claude', 'codex', 'grok', 'augment'] as const;
|
|
47
|
+
|
|
48
|
+
export function isAiKind(value: unknown): value is AiKind {
|
|
49
|
+
return typeof value === 'string' && (AI_KINDS as readonly string[]).includes(value);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* A command name, not a pattern.
|
|
54
|
+
*
|
|
55
|
+
* No spaces and no path separators: the value is matched against one argv
|
|
56
|
+
* token, and anything looser invites an entry that matches every process on
|
|
57
|
+
* the machine. The characters allowed are the ones real binaries use.
|
|
58
|
+
*/
|
|
59
|
+
const NAME_RE = /^[A-Za-z0-9][A-Za-z0-9._+-]{0,63}$/;
|
|
60
|
+
|
|
61
|
+
/** The placeholders expandTranscript understands. Anything else is a typo worth reporting. */
|
|
62
|
+
export const TRANSCRIPT_VARS = ['home', 'cwd', 'tty', 'pid'] as const;
|
|
63
|
+
const VAR_RE = /\{([a-z]+)\}/g;
|
|
64
|
+
|
|
65
|
+
export type CustomAgentParse =
|
|
66
|
+
| { ok: true; agent: CustomAgent }
|
|
67
|
+
| { ok: false; error: string };
|
|
68
|
+
|
|
69
|
+
export function parseCustomAgent(input: unknown): CustomAgentParse {
|
|
70
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
71
|
+
return { ok: false, error: 'A custom agent must be an object' };
|
|
72
|
+
}
|
|
73
|
+
const raw = input as Record<string, unknown>;
|
|
74
|
+
const name = typeof raw.name === 'string' ? raw.name.trim() : '';
|
|
75
|
+
const kind = typeof raw.kind === 'string' ? raw.kind.trim().toLowerCase() : '';
|
|
76
|
+
const transcript = typeof raw.transcript === 'string' ? raw.transcript.trim() : '';
|
|
77
|
+
|
|
78
|
+
if (!NAME_RE.test(name)) {
|
|
79
|
+
return { ok: false, error: 'name must be a bare command name, e.g. sagent (letters, digits, . _ + -)' };
|
|
80
|
+
}
|
|
81
|
+
if (!isAiKind(kind)) {
|
|
82
|
+
return { ok: false, error: `kind must be one of ${AI_KINDS.join(', ')}` };
|
|
83
|
+
}
|
|
84
|
+
if (!transcript) {
|
|
85
|
+
return { ok: false, error: 'transcript is required' };
|
|
86
|
+
}
|
|
87
|
+
// Anchored at {home} so a declaration cannot reach outside the user's own
|
|
88
|
+
// files even before the resolved path is re-checked.
|
|
89
|
+
if (!transcript.startsWith('{home}/')) {
|
|
90
|
+
return { ok: false, error: 'transcript must start with {home}/' };
|
|
91
|
+
}
|
|
92
|
+
if (transcript.includes('..')) {
|
|
93
|
+
return { ok: false, error: 'transcript must not contain ..' };
|
|
94
|
+
}
|
|
95
|
+
// A transcript is a file we parse, so the extension is part of the contract.
|
|
96
|
+
// Without this, `{home}/.ssh/*` is a legal declaration.
|
|
97
|
+
if (!/\.jsonl?$/.test(transcript)) {
|
|
98
|
+
return { ok: false, error: 'transcript must end in .json or .jsonl' };
|
|
99
|
+
}
|
|
100
|
+
// At most one glob, and only in the filename: a `*` in a directory segment
|
|
101
|
+
// would walk the home directory.
|
|
102
|
+
const slash = transcript.lastIndexOf('/');
|
|
103
|
+
if (transcript.slice(0, slash).includes('*')) {
|
|
104
|
+
return { ok: false, error: 'a * may only appear in the file name, not in a directory' };
|
|
105
|
+
}
|
|
106
|
+
if ((transcript.match(/\*/g) || []).length > 1) {
|
|
107
|
+
return { ok: false, error: 'transcript may contain at most one *' };
|
|
108
|
+
}
|
|
109
|
+
const unknown = [...transcript.matchAll(VAR_RE)]
|
|
110
|
+
.map((m) => m[1])
|
|
111
|
+
.filter((v) => !(TRANSCRIPT_VARS as readonly string[]).includes(v));
|
|
112
|
+
if (unknown.length) {
|
|
113
|
+
return {
|
|
114
|
+
ok: false,
|
|
115
|
+
error: `unknown placeholder {${unknown[0]}}; use ${TRANSCRIPT_VARS.map((v) => `{${v}}`).join(', ')}`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, agent: { name, kind, transcript } };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Parses a list, reporting the first bad entry rather than silently dropping it. */
|
|
122
|
+
export function parseCustomAgents(input: unknown): { ok: true; agents: CustomAgent[] } | { ok: false; error: string } {
|
|
123
|
+
if (input === undefined || input === null) return { ok: true, agents: [] };
|
|
124
|
+
if (!Array.isArray(input)) return { ok: false, error: 'customAgents must be an array' };
|
|
125
|
+
const agents: CustomAgent[] = [];
|
|
126
|
+
const seen = new Set<string>();
|
|
127
|
+
for (const entry of input) {
|
|
128
|
+
const parsed = parseCustomAgent(entry);
|
|
129
|
+
if (!parsed.ok) return parsed;
|
|
130
|
+
if (seen.has(parsed.agent.name)) {
|
|
131
|
+
return { ok: false, error: `duplicate custom agent name: ${parsed.agent.name}` };
|
|
132
|
+
}
|
|
133
|
+
seen.add(parsed.agent.name);
|
|
134
|
+
agents.push(parsed.agent);
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, agents };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function escapeRegExp(text: string): string {
|
|
140
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The detector rule for a custom name.
|
|
145
|
+
*
|
|
146
|
+
* Built with the same path-segment anchoring as the built-in rules, so
|
|
147
|
+
* `sagent` and `/usr/local/bin/sagent` match while `vim sagent` and
|
|
148
|
+
* `sagentry` do not. The name is escaped, so a declaration can never inject
|
|
149
|
+
* a pattern — `.*` is matched literally, not as a wildcard.
|
|
150
|
+
*/
|
|
151
|
+
export function customAgentRule(agent: CustomAgent): RegExp {
|
|
152
|
+
return new RegExp(`(^|/)${escapeRegExp(agent.name)}(\\s|$)`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface TranscriptVars {
|
|
156
|
+
home: string;
|
|
157
|
+
cwd?: string;
|
|
158
|
+
tty?: string;
|
|
159
|
+
pid?: number;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Fill in a transcript template.
|
|
164
|
+
*
|
|
165
|
+
* Returns null when a placeholder the template uses has no value — an
|
|
166
|
+
* unresolved `{cwd}` would otherwise be looked up as a literal directory
|
|
167
|
+
* called "{cwd}", which silently never matches and looks like the user's
|
|
168
|
+
* declaration was wrong.
|
|
169
|
+
*/
|
|
170
|
+
export function expandTranscript(template: string, vars: TranscriptVars): string | null {
|
|
171
|
+
let missing = false;
|
|
172
|
+
const out = template.replace(VAR_RE, (_whole, name: string) => {
|
|
173
|
+
const value = name === 'home' ? vars.home
|
|
174
|
+
: name === 'cwd' ? vars.cwd
|
|
175
|
+
: name === 'tty' ? vars.tty
|
|
176
|
+
: name === 'pid' ? (vars.pid === undefined ? undefined : String(vars.pid))
|
|
177
|
+
: undefined;
|
|
178
|
+
if (!value) { missing = true; return ''; }
|
|
179
|
+
return value;
|
|
180
|
+
});
|
|
181
|
+
return missing ? null : out;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Split an expanded template into the directory to list and a matcher for the
|
|
186
|
+
* file name. A template with no `*` still goes through this, so one code path
|
|
187
|
+
* handles both.
|
|
188
|
+
*/
|
|
189
|
+
export function transcriptMatcher(expanded: string): { dir: string; match: (name: string) => boolean } {
|
|
190
|
+
const slash = expanded.lastIndexOf('/');
|
|
191
|
+
const dir = expanded.slice(0, slash) || '/';
|
|
192
|
+
const base = expanded.slice(slash + 1);
|
|
193
|
+
if (!base.includes('*')) {
|
|
194
|
+
return { dir, match: (name: string) => name === base };
|
|
195
|
+
}
|
|
196
|
+
const re = new RegExp(`^${base.split('*').map(escapeRegExp).join('[^/]*')}$`);
|
|
197
|
+
return { dir, match: (name: string) => re.test(name) };
|
|
198
|
+
}
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
export type { AiKind } from './kinds';
|
|
2
|
+
export type { CustomAgent, CustomAgentParse, TranscriptVars } from './custom-agents';
|
|
3
|
+
export {
|
|
4
|
+
AI_KINDS, isAiKind, parseCustomAgent, parseCustomAgents,
|
|
5
|
+
customAgentRule, expandTranscript, transcriptMatcher, TRANSCRIPT_VARS,
|
|
6
|
+
} from './custom-agents';
|
|
2
7
|
|
|
3
8
|
export type { AiStatus, AiEvent, AiAdapter } from './events';
|
|
4
9
|
export { PREVIEW_LIMIT, preview } from './events';
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// break the week a fourth is added — treat an unknown kind as generic at
|
|
3
3
|
// runtime (string on the wire, this union at compile time for the ones we
|
|
4
4
|
// have adapters for).
|
|
5
|
-
export type AiKind = 'claude' | 'codex' | 'grok';
|
|
5
|
+
export type AiKind = 'claude' | 'codex' | 'grok' | 'augment';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@indianic/dialout",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.14.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"
|