@moxxy/sdk 0.8.0 → 0.9.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 +32 -11
- package/dist/cache-strategy.d.ts +9 -0
- package/dist/cache-strategy.d.ts.map +1 -1
- package/dist/compactor-helpers.d.ts.map +1 -1
- package/dist/compactor-helpers.js +5 -0
- package/dist/compactor-helpers.js.map +1 -1
- package/dist/compactor.d.ts +10 -0
- package/dist/compactor.d.ts.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/mode-helpers.d.ts +9 -0
- package/dist/mode-helpers.d.ts.map +1 -1
- package/dist/mode-helpers.js +9 -1
- package/dist/mode-helpers.js.map +1 -1
- package/dist/provider.d.ts +10 -0
- package/dist/provider.d.ts.map +1 -1
- package/dist/session-like.d.ts +32 -0
- package/dist/session-like.d.ts.map +1 -1
- package/dist/subagent.d.ts +15 -0
- package/dist/subagent.d.ts.map +1 -1
- package/dist/tunnel.d.ts +31 -7
- package/dist/tunnel.d.ts.map +1 -1
- package/dist/tunnel.js +153 -1
- package/dist/tunnel.js.map +1 -1
- package/dist/view-renderer.d.ts +15 -0
- package/dist/view-renderer.d.ts.map +1 -1
- package/dist/view-renderer.js +24 -0
- package/dist/view-renderer.js.map +1 -1
- package/dist/workflow.d.ts +73 -6
- package/dist/workflow.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/cache-strategy.ts +9 -0
- package/src/compactor-helpers.ts +5 -0
- package/src/compactor.ts +10 -0
- package/src/index.ts +19 -8
- package/src/mode-helpers.ts +18 -1
- package/src/provider.ts +10 -0
- package/src/session-like.ts +29 -0
- package/src/subagent.ts +16 -0
- package/src/tunnel.test.ts +78 -0
- package/src/tunnel.ts +172 -0
- package/src/view-renderer.ts +22 -0
- package/src/workflow.ts +82 -5
- package/dist/voice.d.ts +0 -36
- package/dist/voice.d.ts.map +0 -1
- package/dist/voice.js +0 -82
- package/dist/voice.js.map +0 -1
- package/src/voice.ts +0 -103
package/src/mode-helpers.ts
CHANGED
|
@@ -383,6 +383,15 @@ export async function collectProviderStream(
|
|
|
383
383
|
* strategy then falls back to its tools/system/tail breakpoints only.
|
|
384
384
|
*/
|
|
385
385
|
stablePrefixIndex?: number;
|
|
386
|
+
/**
|
|
387
|
+
* Number of trailing messages in `messages` that are volatile — injected
|
|
388
|
+
* for this call only (e.g. goal mode's `trailingUserText` nudge) and
|
|
389
|
+
* absent from the append-only log, so they won't recur at the same
|
|
390
|
+
* position next call. Forwarded to the cache strategy as
|
|
391
|
+
* `volatileTailMessageCount` so it keeps its rolling tail breakpoint
|
|
392
|
+
* before them instead of paying a guaranteed-wasted cache write.
|
|
393
|
+
*/
|
|
394
|
+
volatileTailCount?: number;
|
|
386
395
|
} = {},
|
|
387
396
|
): Promise<StreamResult> {
|
|
388
397
|
// Lazy tool gating (opt-in): send only always-on + loaded tool schemas, and
|
|
@@ -410,12 +419,20 @@ export async function collectProviderStream(
|
|
|
410
419
|
...(opts.stablePrefixIndex != null && opts.stablePrefixIndex >= 0
|
|
411
420
|
? { stablePrefixMessageIndex: opts.stablePrefixIndex }
|
|
412
421
|
: {}),
|
|
422
|
+
...(opts.volatileTailCount != null && opts.volatileTailCount > 0
|
|
423
|
+
? { volatileTailMessageCount: opts.volatileTailCount }
|
|
424
|
+
: {}),
|
|
413
425
|
})
|
|
414
426
|
: undefined;
|
|
415
427
|
|
|
428
|
+
// NOTE: `system` is deliberately NOT prefilled with ctx.systemPrompt — the
|
|
429
|
+
// composed system prompt already rides as the leading system-role message
|
|
430
|
+
// (see projectMessages), and providers deliver `req.system` IN ADDITION to
|
|
431
|
+
// message-derived system text. Prefilling it would duplicate the prompt.
|
|
432
|
+
// It stays as the side channel `onBeforeProviderCall` hooks use to inject
|
|
433
|
+
// per-request system text (e.g. the memory consolidation nudge).
|
|
416
434
|
const req = {
|
|
417
435
|
model: ctx.model,
|
|
418
|
-
system: ctx.systemPrompt,
|
|
419
436
|
messages: effectiveMessages,
|
|
420
437
|
...(toolList ? { tools: toolList } : {}),
|
|
421
438
|
...(cacheHints && cacheHints.length > 0 ? { cacheHints } : {}),
|
package/src/provider.ts
CHANGED
|
@@ -38,6 +38,16 @@ export interface CacheHint {
|
|
|
38
38
|
|
|
39
39
|
export interface ProviderRequest {
|
|
40
40
|
readonly model: string;
|
|
41
|
+
/**
|
|
42
|
+
* Extra system text delivered IN ADDITION to any `role: 'system'`
|
|
43
|
+
* messages in `messages`. The loop helpers project the composed system
|
|
44
|
+
* prompt as the leading system message (so cache hints can target it)
|
|
45
|
+
* and leave this unset; `onBeforeProviderCall` hooks use it as the
|
|
46
|
+
* side channel for per-request system injections (e.g. plugin-memory's
|
|
47
|
+
* consolidation nudge), and direct callers (e.g. skill synthesis) may
|
|
48
|
+
* set it instead of crafting a system message. Providers MUST deliver
|
|
49
|
+
* it — appended after the message-derived system text — never drop it.
|
|
50
|
+
*/
|
|
41
51
|
readonly system?: string;
|
|
42
52
|
readonly messages: ReadonlyArray<ProviderMessage>;
|
|
43
53
|
readonly tools?: ReadonlyArray<ToolDef>;
|
package/src/session-like.ts
CHANGED
|
@@ -165,16 +165,45 @@ export interface WorkflowRunView {
|
|
|
165
165
|
readonly steps: ReadonlyArray<{ readonly id: string; readonly status: string; readonly error?: string }>;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
/** Validation result for a draft YAML — backs the visual builder (phase 2). */
|
|
169
|
+
export interface WorkflowValidateView {
|
|
170
|
+
readonly ok: boolean;
|
|
171
|
+
/** One readable line per issue; empty when `ok`. */
|
|
172
|
+
readonly errors: ReadonlyArray<string>;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Result of persisting a workflow from the builder. */
|
|
176
|
+
export interface WorkflowSaveView {
|
|
177
|
+
readonly name: string;
|
|
178
|
+
readonly scope: string;
|
|
179
|
+
readonly path: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
168
182
|
/**
|
|
169
183
|
* The slice of the workflows API a channel needs to drive the `/workflows`
|
|
170
184
|
* modal (list, enable/disable toggle, run). Present on a local Session when
|
|
171
185
|
* `@moxxy/plugin-workflows` is wired; a `RemoteSession` leaves
|
|
172
186
|
* {@link SessionLike.workflows} undefined and the UI degrades gracefully.
|
|
187
|
+
*
|
|
188
|
+
* `validateDraft` / `save` / `getRun` back the upcoming visual workflow
|
|
189
|
+
* builder (phase 2); they are optional so older hosts and remote sessions
|
|
190
|
+
* stay capability-detectable — a channel must feature-check before calling.
|
|
173
191
|
*/
|
|
174
192
|
export interface WorkflowsView {
|
|
175
193
|
list(): Promise<ReadonlyArray<WorkflowSummaryView>>;
|
|
176
194
|
setEnabled(name: string, enabled: boolean): Promise<void>;
|
|
177
195
|
run(name: string): Promise<WorkflowRunView>;
|
|
196
|
+
/** Parse + validate a draft YAML without saving it. */
|
|
197
|
+
validateDraft?(yaml: string): Promise<WorkflowValidateView>;
|
|
198
|
+
/**
|
|
199
|
+
* Persist a workflow from full YAML (create or overwrite). `previousName`
|
|
200
|
+
* (the name the builder loaded) supports rename: when it differs from the
|
|
201
|
+
* YAML's name, the old file + registry entry are removed so a rename doesn't
|
|
202
|
+
* leave an orphaned duplicate.
|
|
203
|
+
*/
|
|
204
|
+
save?(yaml: string, previousName?: string): Promise<WorkflowSaveView>;
|
|
205
|
+
/** Fetch one saved workflow's canonical YAML + on-disk metadata. */
|
|
206
|
+
getRun?(name: string): Promise<{ readonly name: string; readonly scope: string; readonly path: string; readonly yaml: string } | null>;
|
|
178
207
|
}
|
|
179
208
|
|
|
180
209
|
/** One installable plugin in {@link PluginsAdminView.catalog}. */
|
package/src/subagent.ts
CHANGED
|
@@ -26,6 +26,18 @@ export interface SubagentSpec {
|
|
|
26
26
|
readonly allowedTools?: ReadonlyArray<string>;
|
|
27
27
|
/** Human-readable label surfaced in `subagent_*` event payloads. */
|
|
28
28
|
readonly label?: string;
|
|
29
|
+
/**
|
|
30
|
+
* When true, the child session stays alive after the first turn for
|
|
31
|
+
* {@link SubagentSpawner.continue}. `subagent_completed` is deferred until
|
|
32
|
+
* continue or release. Used by the workflow `awaitInput` pause/resume flow.
|
|
33
|
+
*/
|
|
34
|
+
readonly retainSession?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface SubagentContinueArgs {
|
|
38
|
+
readonly childSessionId: SessionId;
|
|
39
|
+
readonly prompt: string;
|
|
40
|
+
readonly label?: string;
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
export interface SubagentResult {
|
|
@@ -43,4 +55,8 @@ export interface SubagentSpawner {
|
|
|
43
55
|
spawn(spec: SubagentSpec): Promise<SubagentResult>;
|
|
44
56
|
/** Run N children in parallel; resolves with results in input order. */
|
|
45
57
|
spawnAll(specs: ReadonlyArray<SubagentSpec>): Promise<ReadonlyArray<SubagentResult>>;
|
|
58
|
+
/** Append a user turn and run a retained child again (requires `retainSession`). */
|
|
59
|
+
continue?(args: SubagentContinueArgs): Promise<SubagentResult>;
|
|
60
|
+
/** Drop a retained session without completing it. */
|
|
61
|
+
release?(childSessionId: SessionId): void;
|
|
46
62
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { isCliTunnelAvailable, spawnCliTunnel } from './tunnel.js';
|
|
3
|
+
|
|
4
|
+
const URL_RE = /https:\/\/[a-z0-9-]+\.example\.test/i;
|
|
5
|
+
|
|
6
|
+
describe('spawnCliTunnel', () => {
|
|
7
|
+
it('resolves the parsed URL + pid when the child prints a matching line', async () => {
|
|
8
|
+
const handle = await spawnCliTunnel({
|
|
9
|
+
cmd: process.execPath,
|
|
10
|
+
// Print a noise line, then the URL, then stay alive so close() can kill it.
|
|
11
|
+
args: ['-e', 'console.log("starting up"); console.log("url=https://abc-123.example.test ready"); setInterval(()=>{}, 1000);'],
|
|
12
|
+
urlRegex: URL_RE,
|
|
13
|
+
name: 'faketunnel',
|
|
14
|
+
timeoutMs: 5_000,
|
|
15
|
+
});
|
|
16
|
+
expect(handle.url).toBe('https://abc-123.example.test');
|
|
17
|
+
expect(handle.pid).toBeGreaterThan(0);
|
|
18
|
+
await handle.close();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('parses URLs printed on stderr too', async () => {
|
|
22
|
+
const handle = await spawnCliTunnel({
|
|
23
|
+
cmd: process.execPath,
|
|
24
|
+
args: ['-e', 'console.error("https://err-host.example.test"); setInterval(()=>{}, 1000);'],
|
|
25
|
+
urlRegex: URL_RE,
|
|
26
|
+
timeoutMs: 5_000,
|
|
27
|
+
});
|
|
28
|
+
expect(handle.url).toBe('https://err-host.example.test');
|
|
29
|
+
await handle.close();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('rejects when the child exits before emitting a URL', async () => {
|
|
33
|
+
await expect(
|
|
34
|
+
spawnCliTunnel({
|
|
35
|
+
cmd: process.execPath,
|
|
36
|
+
args: ['-e', 'process.exit(3)'],
|
|
37
|
+
urlRegex: URL_RE,
|
|
38
|
+
name: 'faketunnel',
|
|
39
|
+
timeoutMs: 5_000,
|
|
40
|
+
}),
|
|
41
|
+
).rejects.toThrow(/faketunnel exited \(code 3\) before emitting a URL/);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('rejects when the executable cannot be spawned', async () => {
|
|
45
|
+
await expect(
|
|
46
|
+
spawnCliTunnel({
|
|
47
|
+
cmd: 'definitely-not-a-real-binary-xyz',
|
|
48
|
+
args: [],
|
|
49
|
+
urlRegex: URL_RE,
|
|
50
|
+
name: 'nope',
|
|
51
|
+
timeoutMs: 2_000,
|
|
52
|
+
}),
|
|
53
|
+
).rejects.toThrow();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects on timeout when no URL is ever printed (and kills the child)', async () => {
|
|
57
|
+
await expect(
|
|
58
|
+
spawnCliTunnel({
|
|
59
|
+
cmd: process.execPath,
|
|
60
|
+
args: ['-e', 'setInterval(()=>{}, 1000);'],
|
|
61
|
+
urlRegex: URL_RE,
|
|
62
|
+
name: 'slowtunnel',
|
|
63
|
+
timeoutMs: 150,
|
|
64
|
+
}),
|
|
65
|
+
).rejects.toThrow(/slowtunnel: timed out after 150ms/);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('isCliTunnelAvailable', () => {
|
|
70
|
+
it('resolves true for an installed runnable binary', async () => {
|
|
71
|
+
// node is always present in the test runtime and supports --version.
|
|
72
|
+
expect(await isCliTunnelAvailable(process.execPath)).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('resolves false for a missing binary', async () => {
|
|
76
|
+
expect(await isCliTunnelAvailable('definitely-not-a-real-binary-xyz')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/tunnel.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* plugins, like every other block); core seeds a `localhost` no-op provider so
|
|
6
6
|
* `getActive()` is non-null.
|
|
7
7
|
*/
|
|
8
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
9
|
+
|
|
8
10
|
export interface TunnelOpenOptions {
|
|
9
11
|
readonly port: number;
|
|
10
12
|
readonly host: string;
|
|
@@ -24,3 +26,173 @@ export interface TunnelProviderDef {
|
|
|
24
26
|
/** Optional readiness gate (e.g. the `cloudflared` binary is installed). */
|
|
25
27
|
isAvailable?(): Promise<boolean>;
|
|
26
28
|
}
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Shared CLI-tunnel subprocess management
|
|
32
|
+
//
|
|
33
|
+
// cloudflared/ngrok (channel-web) and the webhooks tunnel all do the same
|
|
34
|
+
// thing: spawn a CLI, watch its stdout/stderr for the assigned public URL,
|
|
35
|
+
// resolve once it's seen (or reject on timeout/exit/error), and guarantee the
|
|
36
|
+
// child is killed on close *and* on process exit (Node does not reap children).
|
|
37
|
+
// `spawnCliTunnel` is the single implementation they all share.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Guarantees spawned tunnel children (cloudflared/ngrok/…) never orphan or
|
|
42
|
+
* leak. Node does NOT kill child processes when the parent exits, so we track
|
|
43
|
+
* every live child and kill any survivors on process teardown — in addition to
|
|
44
|
+
* the explicit `close()` path. Each tracked child is also force-killed
|
|
45
|
+
* (SIGKILL) if it ignores SIGTERM, so a wedged tunnel can't drain
|
|
46
|
+
* memory/handles.
|
|
47
|
+
*/
|
|
48
|
+
const liveChildren = new Set<ChildProcess>();
|
|
49
|
+
let exitHookInstalled = false;
|
|
50
|
+
|
|
51
|
+
function killChild(child: ChildProcess): void {
|
|
52
|
+
if (child.exitCode != null || child.signalCode != null) return;
|
|
53
|
+
try {
|
|
54
|
+
child.kill('SIGTERM');
|
|
55
|
+
} catch {
|
|
56
|
+
/* already gone */
|
|
57
|
+
}
|
|
58
|
+
// Escalate if it doesn't exit promptly. unref so this timer never holds the
|
|
59
|
+
// event loop open on its own.
|
|
60
|
+
const t = setTimeout(() => {
|
|
61
|
+
try {
|
|
62
|
+
child.kill('SIGKILL');
|
|
63
|
+
} catch {
|
|
64
|
+
/* gone */
|
|
65
|
+
}
|
|
66
|
+
}, 2000);
|
|
67
|
+
t.unref?.();
|
|
68
|
+
child.once('exit', () => clearTimeout(t));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensureExitHook(): void {
|
|
72
|
+
if (exitHookInstalled) return;
|
|
73
|
+
exitHookInstalled = true;
|
|
74
|
+
const killAll = (): void => {
|
|
75
|
+
for (const child of liveChildren) {
|
|
76
|
+
try {
|
|
77
|
+
child.kill('SIGKILL'); // process is exiting; be decisive, no async escalation
|
|
78
|
+
} catch {
|
|
79
|
+
/* gone */
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
liveChildren.clear();
|
|
83
|
+
};
|
|
84
|
+
process.once('exit', killAll);
|
|
85
|
+
process.once('SIGINT', killAll);
|
|
86
|
+
process.once('SIGTERM', killAll);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Track a spawned child; returns an `untrack()` that also kills it cleanly. */
|
|
90
|
+
function trackChild(child: ChildProcess): () => Promise<void> {
|
|
91
|
+
ensureExitHook();
|
|
92
|
+
liveChildren.add(child);
|
|
93
|
+
child.once('exit', () => liveChildren.delete(child));
|
|
94
|
+
return () =>
|
|
95
|
+
new Promise<void>((resolve) => {
|
|
96
|
+
liveChildren.delete(child);
|
|
97
|
+
if (child.exitCode != null || child.signalCode != null) return resolve();
|
|
98
|
+
child.once('exit', () => resolve());
|
|
99
|
+
killChild(child);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SpawnCliTunnelOptions {
|
|
104
|
+
/** Executable to spawn (e.g. `cloudflared`, `ngrok`). */
|
|
105
|
+
readonly cmd: string;
|
|
106
|
+
/** Arguments passed to the executable. */
|
|
107
|
+
readonly args: ReadonlyArray<string>;
|
|
108
|
+
/** Matches the assigned public URL in a chunk of the CLI's stdout/stderr. */
|
|
109
|
+
readonly urlRegex: RegExp;
|
|
110
|
+
/** How long to wait for the URL before giving up. Default 30s. */
|
|
111
|
+
readonly timeoutMs?: number;
|
|
112
|
+
/** Human-readable name used in error messages. Defaults to `cmd`. */
|
|
113
|
+
readonly name?: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A spawned CLI tunnel: the public-URL handle plus the child's pid. */
|
|
117
|
+
export interface CliTunnelHandle extends TunnelHandle {
|
|
118
|
+
/** Spawned process id (-1 if the platform didn't assign one). */
|
|
119
|
+
readonly pid: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const DEFAULT_TUNNEL_URL_TIMEOUT_MS = 30_000;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Spawn a CLI tunnel, parse the assigned public URL out of its output, and
|
|
126
|
+
* resolve a {@link CliTunnelHandle}. The child is tracked so it is killed on
|
|
127
|
+
* `close()`, on tunnel-switch, and on process exit (no orphans). Rejects on
|
|
128
|
+
* timeout, spawn error, or premature exit (the child is killed in every reject
|
|
129
|
+
* path). This is the single spawn-and-parse implementation behind the
|
|
130
|
+
* cloudflared/ngrok tunnel providers and the webhooks tunnel.
|
|
131
|
+
*/
|
|
132
|
+
export function spawnCliTunnel(opts: SpawnCliTunnelOptions): Promise<CliTunnelHandle> {
|
|
133
|
+
const { cmd, args, urlRegex } = opts;
|
|
134
|
+
const name = opts.name ?? cmd;
|
|
135
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_TUNNEL_URL_TIMEOUT_MS;
|
|
136
|
+
|
|
137
|
+
return new Promise<CliTunnelHandle>((resolve, reject) => {
|
|
138
|
+
let child: ChildProcess;
|
|
139
|
+
try {
|
|
140
|
+
child = spawn(cmd, args as string[], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
141
|
+
} catch (err) {
|
|
142
|
+
reject(
|
|
143
|
+
new Error(
|
|
144
|
+
`failed to spawn ${name} — is it installed? (${err instanceof Error ? err.message : String(err)})`,
|
|
145
|
+
),
|
|
146
|
+
);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const untrack = trackChild(child);
|
|
151
|
+
let settled = false;
|
|
152
|
+
|
|
153
|
+
const timer = setTimeout(() => {
|
|
154
|
+
if (settled) return;
|
|
155
|
+
settled = true;
|
|
156
|
+
void untrack();
|
|
157
|
+
reject(new Error(`${name}: timed out after ${timeoutMs}ms waiting for the tunnel URL`));
|
|
158
|
+
}, timeoutMs);
|
|
159
|
+
timer.unref?.();
|
|
160
|
+
|
|
161
|
+
const onData = (buf: Buffer): void => {
|
|
162
|
+
if (settled) return; // drain quietly once resolved so the pipe never fills
|
|
163
|
+
const url = urlRegex.exec(buf.toString('utf8'))?.[0] ?? null;
|
|
164
|
+
if (!url) return;
|
|
165
|
+
settled = true;
|
|
166
|
+
clearTimeout(timer);
|
|
167
|
+
resolve({ url, pid: child.pid ?? -1, close: untrack });
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
child.stdout?.on('data', onData);
|
|
171
|
+
child.stderr?.on('data', onData);
|
|
172
|
+
child.on('error', (err) => {
|
|
173
|
+
if (settled) return;
|
|
174
|
+
settled = true;
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
void untrack();
|
|
177
|
+
reject(err);
|
|
178
|
+
});
|
|
179
|
+
child.on('exit', (code) => {
|
|
180
|
+
if (settled) return;
|
|
181
|
+
settled = true;
|
|
182
|
+
clearTimeout(timer);
|
|
183
|
+
reject(new Error(`${name} exited (code ${code ?? 'null'}) before emitting a URL`));
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Probe whether a tunnel CLI is installed and runnable (`<cmd> --version`
|
|
190
|
+
* exits 0). Used as the `isAvailable()` gate by CLI-backed tunnel providers.
|
|
191
|
+
*/
|
|
192
|
+
export function isCliTunnelAvailable(cmd: string): Promise<boolean> {
|
|
193
|
+
return new Promise<boolean>((resolve) => {
|
|
194
|
+
const child = spawn(cmd, ['--version'], { stdio: 'ignore' });
|
|
195
|
+
child.once('error', () => resolve(false));
|
|
196
|
+
child.once('exit', (code) => resolve(code === 0));
|
|
197
|
+
});
|
|
198
|
+
}
|
package/src/view-renderer.ts
CHANGED
|
@@ -93,6 +93,28 @@ export interface ViewRendererDef {
|
|
|
93
93
|
validate(doc: ViewDoc): ReadonlyArray<ViewParseError>;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* URL-scheme allow-list for agent-authored `href`/`src` values — the single
|
|
98
|
+
* canonical check, enforced at BOTH walls: parse/validate time
|
|
99
|
+
* (core's `parseView`/`validateDoc`) and render time (the web frontend
|
|
100
|
+
* re-checks before emitting an anchor/img; it keeps a verbatim copy in
|
|
101
|
+
* `plugin-channel-web/src/frontend/url-safety.ts` because the browser bundle
|
|
102
|
+
* cannot import the sdk root — keep the two in lockstep).
|
|
103
|
+
*
|
|
104
|
+
* Decision (audit A44): allowed schemes are `https:`, `http:`, `mailto:`,
|
|
105
|
+
* `tel:`, plus relative/fragment URLs; `data:` only as an `img src` and only
|
|
106
|
+
* `data:image/*`. Everything else — notably `javascript:`, `vbscript:`,
|
|
107
|
+
* `data:text/*` — is rejected. Views are designed to be shared with third
|
|
108
|
+
* parties, so a `javascript:` link is click-XSS on a shared surface.
|
|
109
|
+
*/
|
|
110
|
+
export function isSafeViewUrl(url: string, attr: string): boolean {
|
|
111
|
+
const u = url.trim().toLowerCase();
|
|
112
|
+
if (u.startsWith('javascript:') || u.startsWith('vbscript:')) return false;
|
|
113
|
+
if (u.startsWith('data:')) return attr === 'src' && u.startsWith('data:image/');
|
|
114
|
+
if (/^[a-z][a-z0-9+.-]*:/.test(u)) return /^(https?:|mailto:|tel:)/.test(u);
|
|
115
|
+
return true; // relative / fragment
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
const TONE = ['default', 'muted', 'success', 'warn', 'danger'] as const;
|
|
97
119
|
const GAP = ['none', 'sm', 'md', 'lg'] as const;
|
|
98
120
|
|
package/src/workflow.ts
CHANGED
|
@@ -33,11 +33,34 @@ export interface WorkflowTrigger {
|
|
|
33
33
|
/** How a failed step is handled: abort the workflow, skip past it, or retry. */
|
|
34
34
|
export type WorkflowStepErrorMode = 'fail' | 'continue' | 'retry';
|
|
35
35
|
|
|
36
|
+
/** Response format for logic steps (`bridge` only may use `plain`). */
|
|
37
|
+
export type WorkflowLogicStepFormat = 'json' | 'plain';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Bounded iteration node. `body` is an ordered list of step ids that run once
|
|
41
|
+
* per iteration; after each pass the LLM evaluates `condition` as a
|
|
42
|
+
* `then`=continue / `else`=stop predicate (same mechanism as a `condition`
|
|
43
|
+
* step). The loop stops when the predicate says stop OR after `maxIterations`
|
|
44
|
+
* passes — there is no unbounded path. The iteration cap is independent of the
|
|
45
|
+
* nested-workflow {@link WorkflowRunDeps.depth} guard: a loop body that calls
|
|
46
|
+
* nested workflows still obeys the executor's depth cap.
|
|
47
|
+
*/
|
|
48
|
+
export interface WorkflowLoopAction {
|
|
49
|
+
/** Ordered step ids run once per iteration (must resolve; ≥1). */
|
|
50
|
+
readonly body: ReadonlyArray<string>;
|
|
51
|
+
/** LLM predicate: `then` = run another iteration, `else` = stop. */
|
|
52
|
+
readonly condition: string;
|
|
53
|
+
/** Hard cap on iterations (1..50). Hitting it ends the loop cleanly. */
|
|
54
|
+
readonly maxIterations: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
/**
|
|
37
58
|
* One node in the DAG. Exactly one *action* key is set
|
|
38
|
-
* (`skill` | `prompt` | `tool` | `workflow`
|
|
39
|
-
*
|
|
40
|
-
*
|
|
59
|
+
* (`skill` | `prompt` | `tool` | `workflow` | `bridge` | `condition` | `switch`
|
|
60
|
+
* | `loop`). Logic steps run a single no-tools subagent turn; default response
|
|
61
|
+
* is JSON (`vars`, `branch`, optional `text`). `input` is the templated prompt
|
|
62
|
+
* for skill/prompt; `args` for tool/workflow; `bridge` / `condition` / `switch`
|
|
63
|
+
* hold the logic instruction text; `loop` holds a bounded iteration spec.
|
|
41
64
|
*/
|
|
42
65
|
export interface WorkflowStep {
|
|
43
66
|
readonly id: string;
|
|
@@ -45,14 +68,34 @@ export interface WorkflowStep {
|
|
|
45
68
|
readonly prompt?: string;
|
|
46
69
|
readonly tool?: string;
|
|
47
70
|
readonly workflow?: string;
|
|
71
|
+
/** Extract/transform upstream data into `vars` (and optional `text`). */
|
|
72
|
+
readonly bridge?: string;
|
|
73
|
+
/** If/else gate: agent returns `{"branch":"then"|"else"}`. */
|
|
74
|
+
readonly condition?: string;
|
|
75
|
+
readonly then?: ReadonlyArray<string>;
|
|
76
|
+
readonly else?: ReadonlyArray<string>;
|
|
77
|
+
/** Multi-way gate: agent returns `{"branch":"<caseId>"}`. */
|
|
78
|
+
readonly switch?: string;
|
|
79
|
+
readonly cases?: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
80
|
+
readonly default?: ReadonlyArray<string>;
|
|
81
|
+
/** Bounded iteration over `loop.body`, gated by an LLM predicate. */
|
|
82
|
+
readonly loop?: WorkflowLoopAction;
|
|
48
83
|
readonly input?: string;
|
|
49
84
|
readonly args?: Record<string, unknown>;
|
|
50
85
|
readonly needs: ReadonlyArray<string>;
|
|
51
|
-
/**
|
|
86
|
+
/** Legacy deterministic guard DSL; prefer `condition`/`switch` for semantics. */
|
|
52
87
|
readonly when?: string;
|
|
53
88
|
readonly onError: WorkflowStepErrorMode;
|
|
54
89
|
readonly retries: number;
|
|
55
90
|
readonly label?: string;
|
|
91
|
+
/** `plain` only on `bridge`; `condition`/`switch` always require JSON. */
|
|
92
|
+
readonly format?: WorkflowLogicStepFormat;
|
|
93
|
+
/**
|
|
94
|
+
* When true on a `prompt` or `skill` step, the DAG pauses after the
|
|
95
|
+
* subagent's first turn so the operator can reply once; the step completes
|
|
96
|
+
* after a follow-up turn. Ignored on logic / `tool` / `workflow` steps.
|
|
97
|
+
*/
|
|
98
|
+
readonly awaitInput?: boolean;
|
|
56
99
|
}
|
|
57
100
|
|
|
58
101
|
export interface WorkflowInputSpec {
|
|
@@ -67,6 +110,27 @@ export interface WorkflowDelivery {
|
|
|
67
110
|
readonly inbox: boolean;
|
|
68
111
|
}
|
|
69
112
|
|
|
113
|
+
/** Visual editor metadata. Runtime executors must ignore this field. */
|
|
114
|
+
export interface WorkflowUiLayoutNode {
|
|
115
|
+
readonly x: number;
|
|
116
|
+
readonly y: number;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export interface WorkflowUiViewport {
|
|
120
|
+
readonly x: number;
|
|
121
|
+
readonly y: number;
|
|
122
|
+
readonly zoom: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface WorkflowUiLayout {
|
|
126
|
+
readonly nodes: Record<string, WorkflowUiLayoutNode>;
|
|
127
|
+
readonly viewport?: WorkflowUiViewport;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface WorkflowUi {
|
|
131
|
+
readonly layout?: WorkflowUiLayout;
|
|
132
|
+
}
|
|
133
|
+
|
|
70
134
|
export interface Workflow {
|
|
71
135
|
readonly name: string;
|
|
72
136
|
readonly description: string;
|
|
@@ -80,6 +144,8 @@ export interface Workflow {
|
|
|
80
144
|
readonly inputs: Record<string, WorkflowInputSpec>;
|
|
81
145
|
readonly on?: WorkflowTrigger;
|
|
82
146
|
readonly delivery?: WorkflowDelivery;
|
|
147
|
+
/** GUI-only metadata persisted with the YAML artifact. */
|
|
148
|
+
readonly ui?: WorkflowUi;
|
|
83
149
|
/** Max steps to run concurrently in one ready-set round. */
|
|
84
150
|
readonly concurrency: number;
|
|
85
151
|
readonly steps: ReadonlyArray<WorkflowStep>;
|
|
@@ -104,6 +170,9 @@ export type WorkflowEventSubtype =
|
|
|
104
170
|
| 'workflow_step_completed'
|
|
105
171
|
| 'workflow_step_skipped'
|
|
106
172
|
| 'workflow_step_failed'
|
|
173
|
+
| 'workflow_step_awaiting_input'
|
|
174
|
+
| 'workflow_paused'
|
|
175
|
+
| 'workflow_resumed'
|
|
107
176
|
| 'workflow_completed'
|
|
108
177
|
| 'workflow_failed';
|
|
109
178
|
|
|
@@ -134,7 +203,9 @@ export interface WorkflowRunDeps {
|
|
|
134
203
|
readonly depth?: number;
|
|
135
204
|
}
|
|
136
205
|
|
|
137
|
-
export type WorkflowStepStatus = 'completed' | 'skipped' | 'failed';
|
|
206
|
+
export type WorkflowStepStatus = 'completed' | 'skipped' | 'failed' | 'awaiting_input';
|
|
207
|
+
|
|
208
|
+
export type WorkflowRunStatus = 'completed' | 'paused' | 'failed';
|
|
138
209
|
|
|
139
210
|
export interface WorkflowStepResult {
|
|
140
211
|
readonly id: string;
|
|
@@ -147,10 +218,16 @@ export interface WorkflowStepResult {
|
|
|
147
218
|
|
|
148
219
|
export interface WorkflowRunResult {
|
|
149
220
|
readonly ok: boolean;
|
|
221
|
+
readonly status: WorkflowRunStatus;
|
|
150
222
|
readonly steps: ReadonlyArray<WorkflowStepResult>;
|
|
151
223
|
/** Output of the terminal (sink) step(s) — what delivery sends. */
|
|
152
224
|
readonly output: string;
|
|
153
225
|
readonly error?: string;
|
|
226
|
+
/** Set when `status` is `paused` — use with workflow reply API. */
|
|
227
|
+
readonly runId?: string;
|
|
228
|
+
readonly pendingStepId?: string;
|
|
229
|
+
/** Child subagent session id for chat / permissions while paused. */
|
|
230
|
+
readonly interactionAgentId?: string;
|
|
154
231
|
}
|
|
155
232
|
|
|
156
233
|
/**
|
package/dist/voice.d.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Voice / transcription helpers shared across surfaces.
|
|
3
|
-
*
|
|
4
|
-
* The TUI's voice-input infrastructure used to inline the same logic
|
|
5
|
-
* with a `Codex`-specific name baked in. Pulled out here as
|
|
6
|
-
* *agnostic* helpers that take a transcriber name as input, so the
|
|
7
|
-
* desktop, TUI, and any future channel can mirror the same flow:
|
|
8
|
-
*
|
|
9
|
-
* - Is the session ready to transcribe? Check via the requirements
|
|
10
|
-
* API for a named transcriber. (`checkTranscriberReady`)
|
|
11
|
-
* - Activate any registered transcriber lazily. Returns the active
|
|
12
|
-
* transcriber instance. (`resolveTranscriber`)
|
|
13
|
-
* - "Just give me whatever works" — try a list of candidates in
|
|
14
|
-
* order, or fall back to the first registered one. (`pickFirstAvailableTranscriber`)
|
|
15
|
-
*/
|
|
16
|
-
import type { ClientSession } from './client-session.js';
|
|
17
|
-
import type { MoxxyRequirement, RequirementCheck } from './requirements.js';
|
|
18
|
-
import type { Transcriber } from './transcriber.js';
|
|
19
|
-
/** Probe whether a *named* transcriber is ready: registered, with any
|
|
20
|
-
* declared upstream requirements satisfied. The optional `requires`
|
|
21
|
-
* list lets channels gate on additional provider / auth runtimes
|
|
22
|
-
* (the Codex transcriber e.g. depends on the `openai-codex` provider
|
|
23
|
-
* + its OAuth runtime). */
|
|
24
|
-
export declare function checkTranscriberReady(session: ClientSession, transcriberName: string, requires?: ReadonlyArray<MoxxyRequirement>): RequirementCheck;
|
|
25
|
-
/** Activate a transcriber by name, lazily. Returns the active instance
|
|
26
|
-
* ready to `.transcribe(...)`. Throws if no such transcriber is
|
|
27
|
-
* registered, or a *different* one is already active. */
|
|
28
|
-
export declare function resolveTranscriber(session: ClientSession, transcriberName: string): Transcriber;
|
|
29
|
-
/** "Just pick a transcriber that works."
|
|
30
|
-
*
|
|
31
|
-
* Tries each name in `candidates` in order — first one that can be
|
|
32
|
-
* activated wins. Returns null if none can be activated, so callers
|
|
33
|
-
* can degrade gracefully (hide their mic button, show a "no voice
|
|
34
|
-
* configured" tip, …) instead of throwing. */
|
|
35
|
-
export declare function pickFirstAvailableTranscriber(session: ClientSession, candidates: ReadonlyArray<string>): Transcriber | null;
|
|
36
|
-
//# sourceMappingURL=voice.d.ts.map
|
package/dist/voice.d.ts.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"voice.d.ts","sourceRoot":"","sources":["../src/voice.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACzD,OAAO,KAAK,EACV,gBAAgB,EAChB,gBAAgB,EAEjB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAEpD;;;;4BAI4B;AAC5B,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,MAAM,EACvB,QAAQ,GAAE,aAAa,CAAC,gBAAgB,CAAM,GAC7C,gBAAgB,CAqBlB;AAED;;0DAE0D;AAC1D,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,aAAa,EACtB,eAAe,EAAE,MAAM,GACtB,WAAW,CAcb;AAED;;;;;+CAK+C;AAC/C,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,aAAa,EACtB,UAAU,EAAE,aAAa,CAAC,MAAM,CAAC,GAChC,WAAW,GAAG,IAAI,CAepB"}
|