@parall/cli 1.45.0 → 1.47.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/commands/clip.d.ts.map +1 -1
- package/dist/commands/clip.js +5 -1
- package/dist/commands/slack.d.ts +10 -0
- package/dist/commands/slack.d.ts.map +1 -0
- package/dist/commands/slack.js +131 -0
- package/dist/index.js +2 -0
- package/dist/lib/clip-invoke.d.ts +45 -0
- package/dist/lib/clip-invoke.d.ts.map +1 -0
- package/dist/lib/clip-invoke.js +61 -0
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/commands/clip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/commands/clip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA+BpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA0JpD"}
|
package/dist/commands/clip.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolveCredentials } from '../lib/client.js';
|
|
2
|
+
import { invokeClipAwaitingActivation } from '../lib/clip-invoke.js';
|
|
2
3
|
import { printJson, printError } from '../lib/output.js';
|
|
3
4
|
/**
|
|
4
5
|
* Derive a CLI-friendly alias from a registry source string.
|
|
@@ -143,7 +144,10 @@ export function registerClipCommands(program) {
|
|
|
143
144
|
input: parsedInput,
|
|
144
145
|
timeout_ms: timeoutMs,
|
|
145
146
|
};
|
|
146
|
-
|
|
147
|
+
// Hosted-browser clips may hit a cold profile: absorb the (pre-dispatch,
|
|
148
|
+
// side-effect-free) BROWSER_PROFILE_ACTIVATING window here instead of
|
|
149
|
+
// making every agent hand-roll a retry loop. See lib/clip-invoke.ts.
|
|
150
|
+
const result = await invokeClipAwaitingActivation(client, orgId, req);
|
|
147
151
|
if (result.error) {
|
|
148
152
|
printError(new Error(result.error));
|
|
149
153
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
/**
|
|
3
|
+
* `parall slack …` — the per-vendor platform verb for a channel whose vendor
|
|
4
|
+
* ships no agent-grade CLI (tier B in the multi-channel architecture). The
|
|
5
|
+
* agent sends as its bound bot identity; Slack credentials never reach the
|
|
6
|
+
* runtime — api-server performs the vendor call in-process.
|
|
7
|
+
* Design: docs/engineering-design/multi-channel-architecture-design.md §8.2.
|
|
8
|
+
*/
|
|
9
|
+
export declare function registerSlackCommands(program: Command): void;
|
|
10
|
+
//# sourceMappingURL=slack.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"slack.d.ts","sourceRoot":"","sources":["../../src/commands/slack.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,OAAO,QAgIrD"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { resolveCredentials } from '../lib/client.js';
|
|
2
|
+
import { printError, printJson } from '../lib/output.js';
|
|
3
|
+
/**
|
|
4
|
+
* `parall slack …` — the per-vendor platform verb for a channel whose vendor
|
|
5
|
+
* ships no agent-grade CLI (tier B in the multi-channel architecture). The
|
|
6
|
+
* agent sends as its bound bot identity; Slack credentials never reach the
|
|
7
|
+
* runtime — api-server performs the vendor call in-process.
|
|
8
|
+
* Design: docs/engineering-design/multi-channel-architecture-design.md §8.2.
|
|
9
|
+
*/
|
|
10
|
+
export function registerSlackCommands(program) {
|
|
11
|
+
const slack = program
|
|
12
|
+
.command('slack')
|
|
13
|
+
.description('Slack channel platform verbs (agent-only; sends as the bound bot)');
|
|
14
|
+
slack
|
|
15
|
+
.command('send')
|
|
16
|
+
.description('Send a message to a Slack conversation this connection has seen inbound')
|
|
17
|
+
.requiredOption('--channel <channelId>', 'Vendor-native conversation id from the inbound event (C…/G…/D…)')
|
|
18
|
+
.requiredOption('--text <text>', 'Message text (plain text)')
|
|
19
|
+
.option('--reply-to <messageId>', 'Inbound message id you are answering ({channel}:{ts}, thread children {channel}:{root}#{ts}); REQUIRED in channels (reply lands in its thread). In DMs pass it too: a thread-child id lands the reply in that thread, a bare id keeps the main flow — the session never forks either way')
|
|
20
|
+
.action(async (opts) => {
|
|
21
|
+
try {
|
|
22
|
+
const { client, orgId } = resolveCredentials();
|
|
23
|
+
const sent = await client.sendChannelMessage(orgId, {
|
|
24
|
+
channel_type: 'slack',
|
|
25
|
+
conversation_id: opts.channel,
|
|
26
|
+
text: opts.text,
|
|
27
|
+
...(opts.replyTo ? { reply_to: opts.replyTo } : {}),
|
|
28
|
+
});
|
|
29
|
+
printJson(sent);
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
printError(err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
// Read verbs: workspace visibility as the bot sees it. Authorization is
|
|
36
|
+
// the bot's own Slack permissions (invite it to a channel to read that
|
|
37
|
+
// channel's history) — reads are not limited to seen conversations.
|
|
38
|
+
// Fail fast on a malformed --limit instead of silently falling back to
|
|
39
|
+
// the server default page size.
|
|
40
|
+
const page = (opts) => {
|
|
41
|
+
let limit;
|
|
42
|
+
if (opts.limit !== undefined) {
|
|
43
|
+
limit = Number(opts.limit);
|
|
44
|
+
if (!Number.isInteger(limit) || limit < 1 || limit > 200) {
|
|
45
|
+
throw new Error('--limit must be an integer between 1 and 200');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
...(opts.cursor ? { cursor: opts.cursor } : {}),
|
|
50
|
+
...(limit !== undefined ? { limit } : {}),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
slack
|
|
54
|
+
.command('channels')
|
|
55
|
+
.description('List workspace channels as the bot sees them (is_member marks joined ones)')
|
|
56
|
+
.option('--cursor <cursor>', 'Pagination cursor from a previous page')
|
|
57
|
+
.option('--limit <n>', 'Page size (max 200)')
|
|
58
|
+
.action(async (opts) => {
|
|
59
|
+
try {
|
|
60
|
+
const { client, orgId } = resolveCredentials();
|
|
61
|
+
printJson(await client.listSlackChannels(orgId, page(opts)));
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
printError(err);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
slack
|
|
68
|
+
.command('users')
|
|
69
|
+
.description('List workspace members')
|
|
70
|
+
.option('--cursor <cursor>', 'Pagination cursor from a previous page')
|
|
71
|
+
.option('--limit <n>', 'Page size (max 200)')
|
|
72
|
+
.action(async (opts) => {
|
|
73
|
+
try {
|
|
74
|
+
const { client, orgId } = resolveCredentials();
|
|
75
|
+
printJson(await client.listSlackUsers(orgId, page(opts)));
|
|
76
|
+
}
|
|
77
|
+
catch (err) {
|
|
78
|
+
printError(err);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
slack
|
|
82
|
+
.command('history')
|
|
83
|
+
.description('Read a conversation message history (bot must be able to see it)')
|
|
84
|
+
.requiredOption('--conversation <id>', 'Vendor-native conversation id (C…/G…/D…)')
|
|
85
|
+
.option('--cursor <cursor>', 'Pagination cursor from a previous page')
|
|
86
|
+
.option('--limit <n>', 'Page size (max 200)')
|
|
87
|
+
.action(async (opts) => {
|
|
88
|
+
try {
|
|
89
|
+
const { client, orgId } = resolveCredentials();
|
|
90
|
+
printJson(await client.slackHistory(orgId, opts.conversation, page(opts)));
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
printError(err);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
slack
|
|
97
|
+
.command('status')
|
|
98
|
+
.description('Set/clear the Agents-pane typing indicator on an assistant thread (cosmetic)')
|
|
99
|
+
.requiredOption('--conversation <id>', 'DM conversation id (D…)')
|
|
100
|
+
.requiredOption('--thread <rootTs>', "The assistant thread's root ts")
|
|
101
|
+
.option('--text <status>', 'Status text (omit to clear)', '')
|
|
102
|
+
.action(async (opts) => {
|
|
103
|
+
try {
|
|
104
|
+
const { client, orgId } = resolveCredentials();
|
|
105
|
+
await client.setSlackStatus(orgId, {
|
|
106
|
+
conversation: opts.conversation,
|
|
107
|
+
thread_ts: opts.thread,
|
|
108
|
+
status: opts.text ?? '',
|
|
109
|
+
});
|
|
110
|
+
printJson({ ok: true });
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
printError(err);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
slack
|
|
117
|
+
.command('members')
|
|
118
|
+
.description('List a conversation member ids (resolve names via `parall slack users`)')
|
|
119
|
+
.requiredOption('--conversation <id>', 'Vendor-native conversation id (C…/G…)')
|
|
120
|
+
.option('--cursor <cursor>', 'Pagination cursor from a previous page')
|
|
121
|
+
.option('--limit <n>', 'Page size (max 200)')
|
|
122
|
+
.action(async (opts) => {
|
|
123
|
+
try {
|
|
124
|
+
const { client, orgId } = resolveCredentials();
|
|
125
|
+
printJson(await client.slackMembers(orgId, opts.conversation, page(opts)));
|
|
126
|
+
}
|
|
127
|
+
catch (err) {
|
|
128
|
+
printError(err);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,7 @@ import { registerSearchCommands } from './commands/search.js';
|
|
|
20
20
|
import { registerFileCommands } from './commands/files.js';
|
|
21
21
|
import { registerMachineCommands } from './commands/machines.js';
|
|
22
22
|
import { registerClipCommands } from './commands/clip.js';
|
|
23
|
+
import { registerSlackCommands } from './commands/slack.js';
|
|
23
24
|
const require = createRequire(import.meta.url);
|
|
24
25
|
const pkg = require('../package.json');
|
|
25
26
|
const program = new Command();
|
|
@@ -46,4 +47,5 @@ registerSearchCommands(program);
|
|
|
46
47
|
registerFileCommands(program);
|
|
47
48
|
registerMachineCommands(program);
|
|
48
49
|
registerClipCommands(program);
|
|
50
|
+
registerSlackCommands(program);
|
|
49
51
|
program.parse();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { InvokeClipRequest, InvokeClipResponse, ParallClient } from '@parall/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Hosted browser activation wait — CLI-side bounded retry for `clip invoke`.
|
|
4
|
+
*
|
|
5
|
+
* A clip bound to a `placement=hosted` BrowserProfile whose pod is cold returns
|
|
6
|
+
* 503 BROWSER_PROFILE_ACTIVATING from the invoke pre-flight: the controller is
|
|
7
|
+
* spawning + registering the pod, and the clip command has NOT been dispatched
|
|
8
|
+
* yet, so retrying the same request cannot double-execute anything. This module
|
|
9
|
+
* absorbs that one precise error so a single `parall clip invoke` rides through
|
|
10
|
+
* cold start instead of forcing the caller (agent or human) into a blind
|
|
11
|
+
* retry loop.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT in the SDK: `invokeClip()` keeps its throw-on-503 semantics
|
|
14
|
+
* for every other consumer; only the CLI invoke behavior waits.
|
|
15
|
+
*/
|
|
16
|
+
/** Total activation wait budget. Independent of the clip command --timeout:
|
|
17
|
+
* each attempt keeps its own full command timeout, so worst-case wall clock is
|
|
18
|
+
* roughly this budget plus one command timeout. */
|
|
19
|
+
export declare const ACTIVATION_WAIT_BUDGET_MS = 60000;
|
|
20
|
+
/** Backoff schedule between activation retries; the last entry repeats. */
|
|
21
|
+
export declare const ACTIVATION_BACKOFF_MS: number[];
|
|
22
|
+
/** Injectable time hooks so tests never really sleep. */
|
|
23
|
+
export type ActivationWaitHooks = {
|
|
24
|
+
sleep?: (ms: number) => Promise<void>;
|
|
25
|
+
now?: () => number;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* The ONLY retryable error: a typed ApiError carrying exactly 503 +
|
|
29
|
+
* BROWSER_PROFILE_ACTIVATING. Everything else — consent, permission, provider
|
|
30
|
+
* offline/not-running, command errors, timeouts, other 5xx — means retrying is
|
|
31
|
+
* either pointless or unsafe (the command may have been dispatched), so it
|
|
32
|
+
* propagates unchanged on the first occurrence.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isActivationPending(err: unknown): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Invoke a clip, transparently absorbing the hosted-browser activation window.
|
|
37
|
+
*
|
|
38
|
+
* Retries the identical request while the server reports
|
|
39
|
+
* BROWSER_PROFILE_ACTIVATING, backing off 500ms → 1s → 2s (capped) until the
|
|
40
|
+
* 60s activation budget is spent. Once spent, the LAST typed ApiError is
|
|
41
|
+
* rethrown as-is — status/code intact — so the printed error stays
|
|
42
|
+
* machine-readable. Any other failure is rethrown immediately.
|
|
43
|
+
*/
|
|
44
|
+
export declare function invokeClipAwaitingActivation(client: Pick<ParallClient, 'invokeClip'>, orgId: string, req: InvokeClipRequest, hooks?: ActivationWaitHooks): Promise<InvokeClipResponse>;
|
|
45
|
+
//# sourceMappingURL=clip-invoke.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clip-invoke.d.ts","sourceRoot":"","sources":["../../src/lib/clip-invoke.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGvF;;;;;;;;;;;;;GAaG;AAEH;;oDAEoD;AACpD,eAAO,MAAM,yBAAyB,QAAS,CAAC;AAEhD,2EAA2E;AAC3E,eAAO,MAAM,qBAAqB,UAAsB,CAAC;AAEzD,yDAAyD;AACzD,MAAM,MAAM,mBAAmB,GAAG;IAChC,KAAK,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAIF;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAEzD;AAED;;;;;;;;GAQG;AACH,wBAAsB,4BAA4B,CAChD,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,EACxC,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,iBAAiB,EACtB,KAAK,CAAC,EAAE,mBAAmB,GAC1B,OAAO,CAAC,kBAAkB,CAAC,CAiB7B"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { ApiError } from '@parall/sdk';
|
|
2
|
+
/**
|
|
3
|
+
* Hosted browser activation wait — CLI-side bounded retry for `clip invoke`.
|
|
4
|
+
*
|
|
5
|
+
* A clip bound to a `placement=hosted` BrowserProfile whose pod is cold returns
|
|
6
|
+
* 503 BROWSER_PROFILE_ACTIVATING from the invoke pre-flight: the controller is
|
|
7
|
+
* spawning + registering the pod, and the clip command has NOT been dispatched
|
|
8
|
+
* yet, so retrying the same request cannot double-execute anything. This module
|
|
9
|
+
* absorbs that one precise error so a single `parall clip invoke` rides through
|
|
10
|
+
* cold start instead of forcing the caller (agent or human) into a blind
|
|
11
|
+
* retry loop.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately NOT in the SDK: `invokeClip()` keeps its throw-on-503 semantics
|
|
14
|
+
* for every other consumer; only the CLI invoke behavior waits.
|
|
15
|
+
*/
|
|
16
|
+
/** Total activation wait budget. Independent of the clip command --timeout:
|
|
17
|
+
* each attempt keeps its own full command timeout, so worst-case wall clock is
|
|
18
|
+
* roughly this budget plus one command timeout. */
|
|
19
|
+
export const ACTIVATION_WAIT_BUDGET_MS = 60_000;
|
|
20
|
+
/** Backoff schedule between activation retries; the last entry repeats. */
|
|
21
|
+
export const ACTIVATION_BACKOFF_MS = [500, 1_000, 2_000];
|
|
22
|
+
const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
23
|
+
/**
|
|
24
|
+
* The ONLY retryable error: a typed ApiError carrying exactly 503 +
|
|
25
|
+
* BROWSER_PROFILE_ACTIVATING. Everything else — consent, permission, provider
|
|
26
|
+
* offline/not-running, command errors, timeouts, other 5xx — means retrying is
|
|
27
|
+
* either pointless or unsafe (the command may have been dispatched), so it
|
|
28
|
+
* propagates unchanged on the first occurrence.
|
|
29
|
+
*/
|
|
30
|
+
export function isActivationPending(err) {
|
|
31
|
+
return err instanceof ApiError && err.status === 503 && err.code === 'BROWSER_PROFILE_ACTIVATING';
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Invoke a clip, transparently absorbing the hosted-browser activation window.
|
|
35
|
+
*
|
|
36
|
+
* Retries the identical request while the server reports
|
|
37
|
+
* BROWSER_PROFILE_ACTIVATING, backing off 500ms → 1s → 2s (capped) until the
|
|
38
|
+
* 60s activation budget is spent. Once spent, the LAST typed ApiError is
|
|
39
|
+
* rethrown as-is — status/code intact — so the printed error stays
|
|
40
|
+
* machine-readable. Any other failure is rethrown immediately.
|
|
41
|
+
*/
|
|
42
|
+
export async function invokeClipAwaitingActivation(client, orgId, req, hooks) {
|
|
43
|
+
const sleep = hooks?.sleep ?? realSleep;
|
|
44
|
+
const now = hooks?.now ?? Date.now;
|
|
45
|
+
const deadline = now() + ACTIVATION_WAIT_BUDGET_MS;
|
|
46
|
+
for (let attempt = 0;; attempt++) {
|
|
47
|
+
try {
|
|
48
|
+
return await client.invokeClip(orgId, req);
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (!isActivationPending(err))
|
|
52
|
+
throw err;
|
|
53
|
+
const remaining = deadline - now();
|
|
54
|
+
if (remaining <= 0)
|
|
55
|
+
throw err;
|
|
56
|
+
const backoff = ACTIVATION_BACKOFF_MS[Math.min(attempt, ACTIVATION_BACKOFF_MS.length - 1)];
|
|
57
|
+
// Clamp to the remaining budget so the final sleep can't overrun it.
|
|
58
|
+
await sleep(Math.min(backoff, remaining));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@parall/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.47.0",
|
|
4
4
|
"description": "CLI client for Parall — universal agent & human access to Parall API",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,14 +36,14 @@
|
|
|
36
36
|
"diff": "^8.0.3",
|
|
37
37
|
"js-yaml": "^4.1.0",
|
|
38
38
|
"zod": "^4.3.6",
|
|
39
|
-
"@parall/agent-core": "1.
|
|
40
|
-
"@parall/sdk": "1.
|
|
39
|
+
"@parall/agent-core": "1.47.0",
|
|
40
|
+
"@parall/sdk": "1.47.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/js-yaml": "^4.0.9",
|
|
44
44
|
"@types/node": "^22.0.0",
|
|
45
45
|
"typescript": "^5.7.0",
|
|
46
|
-
"@parall/agent-core": "1.
|
|
46
|
+
"@parall/agent-core": "1.47.0"
|
|
47
47
|
},
|
|
48
48
|
"scripts": {
|
|
49
49
|
"build": "tsc",
|