@parall/cli 1.47.0 → 1.49.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.
@@ -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;AA+BpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA0JpD"}
1
+ {"version":3,"file":"clip.d.ts","sourceRoot":"","sources":["../../src/commands/clip.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgCpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA4OpD"}
@@ -1,6 +1,7 @@
1
1
  import { resolveCredentials } from '../lib/client.js';
2
2
  import { invokeClipAwaitingActivation } from '../lib/clip-invoke.js';
3
- import { printJson, printError } from '../lib/output.js';
3
+ import { execEdgeClipAwaitingActivation } from '../lib/edge-exec.js';
4
+ import { printJson, printError, parsePositiveInt } from '../lib/output.js';
4
5
  /**
5
6
  * Derive a CLI-friendly alias from a registry source string.
6
7
  * Handles scoped packages ("@scope/name@1.0.0" → "name") and
@@ -159,6 +160,66 @@ export function registerClipCommands(program) {
159
160
  printError(err);
160
161
  }
161
162
  });
163
+ clip
164
+ .command('exec')
165
+ .description('Execute a registry (Edge) clip command on an Edge device. A cloud (hosted) profile is reachable ONLY via --connection; with neither --connection nor --edge, the server resolves just your OWN online desktop device (legacy BYOC fallback — never a cloud profile)')
166
+ .argument('<clip>', 'Clip name in the org clip registry')
167
+ .argument('<command>', 'Command name to execute')
168
+ .argument('[args]', 'Command arguments (JSON string or plain text)')
169
+ .option('--connection <ref>', 'Clip connection id (ccn_…) or alias. REQUIRED to reach a cloud (hosted) profile — the binding its maintainer created is the authorization')
170
+ .option('--edge <edgeId>', 'A desktop (BYOC) device you own. Mutually exclusive with --connection')
171
+ .option('--profile <name>', 'Browser profile (with --connection it may only restate the granted one)')
172
+ .option('--timeout <ms>', 'Execution timeout in milliseconds', '30000')
173
+ .action(async (clipName, command, args, opts) => {
174
+ try {
175
+ const { client, orgId } = resolveCredentials();
176
+ let parsedArgs;
177
+ if (args !== undefined) {
178
+ try {
179
+ parsedArgs = JSON.parse(args);
180
+ }
181
+ catch {
182
+ parsedArgs = args;
183
+ }
184
+ }
185
+ // Strict grammar, validated BEFORE the wire: parseInt would accept
186
+ // "5000ms" or "1.5", and the server silently replaces an out-of-range
187
+ // timeout with its 30s default — so a malformed flag would run with a
188
+ // deadline the caller never asked for. printError never returns.
189
+ let timeoutMs;
190
+ if (opts?.timeout) {
191
+ timeoutMs = parsePositiveInt(opts.timeout);
192
+ if (timeoutMs === undefined || timeoutMs > 120_000) {
193
+ printError(new Error('--timeout must be an integer between 1 and 120000 (milliseconds)'));
194
+ }
195
+ }
196
+ const req = {
197
+ clip: clipName,
198
+ command,
199
+ args: parsedArgs,
200
+ connection: opts?.connection,
201
+ edge_id: opts?.edge,
202
+ profile: opts?.profile,
203
+ timeout: timeoutMs,
204
+ };
205
+ // A cold cloud profile answers 503 EDGE_ACTIVATING BEFORE anything is
206
+ // dispatched; absorb that one retryable window here (bounded, one
207
+ // correlation id) instead of making every agent hand-roll a loop.
208
+ // Everything else — OUTCOME_UNKNOWN above all — propagates untouched:
209
+ // a dispatched command may have executed, and retrying it is not this
210
+ // tool's call. See lib/edge-exec.ts.
211
+ const result = await execEdgeClipAwaitingActivation(client, orgId, req);
212
+ if (result.success) {
213
+ printJson(result.data ?? null);
214
+ }
215
+ else {
216
+ printError(new Error(result.error ?? 'execution failed'));
217
+ }
218
+ }
219
+ catch (err) {
220
+ printError(err);
221
+ }
222
+ });
162
223
  clip
163
224
  .command('remove')
164
225
  .description('Remove a clip by alias')
@@ -1,4 +1,20 @@
1
1
  import { Command } from 'commander';
2
+ /**
3
+ * Decide how `comments list` addresses its target. A wiki file URI without an
4
+ * anchor lists by PREFIX so inline anchored comments
5
+ * (`prll://wik_x/path.md?rev=<sha>#h=...`) are returned alongside page-level
6
+ * ones — the server matches `prefix` OR `prefix?%` OR `prefix#%`. Everything
7
+ * else (task/message targets, wiki roots, changesets, and wiki URIs that carry
8
+ * an explicit `?rev`/`#` anchor) keeps exact `target_uri` matching, as does
9
+ * `--exact`. Exported for tests.
10
+ */
11
+ export declare function buildListTarget(target: string, opts?: {
12
+ exact?: boolean;
13
+ }): {
14
+ target_uri: string;
15
+ } | {
16
+ target_prefix: string;
17
+ };
2
18
  /**
3
19
  * Generic, target-agnostic comment commands over `POST/GET/PATCH/DELETE
4
20
  * /comments`. Unlike `parall tasks comments` (task-only) and `parall wiki
@@ -1 +1 @@
1
- {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/commands/comments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgBpC;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAoGvD"}
1
+ {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../../src/commands/comments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA2BpC;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GACzB;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,aAAa,EAAE,MAAM,CAAA;CAAE,CAMpD;AAED;;;;;;GAMG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAsHvD"}
@@ -10,6 +10,34 @@ function stripUndefined(obj) {
10
10
  function normalizeTarget(target) {
11
11
  return target.startsWith('prll://') ? target : `prll://${target}`;
12
12
  }
13
+ // A wiki FILE URI with no explicit anchor: `prll://wik_<id>/<path>` carrying
14
+ // no query (`?rev=`) and no fragment (`#h=` / `#l=`). Wiki roots (`prll://wik_x`,
15
+ // `prll://wik_x/`) and directory-ish paths (trailing slash) don't qualify — the
16
+ // server's target_prefix ACL only accepts file-scope prefixes.
17
+ function isWikiFileUriWithoutAnchor(uri) {
18
+ if (!uri.startsWith('prll://wik_'))
19
+ return false;
20
+ if (uri.includes('?') || uri.includes('#'))
21
+ return false;
22
+ const path = uri.slice('prll://'.length).split('/').slice(1).join('/');
23
+ return path !== '' && !path.endsWith('/');
24
+ }
25
+ /**
26
+ * Decide how `comments list` addresses its target. A wiki file URI without an
27
+ * anchor lists by PREFIX so inline anchored comments
28
+ * (`prll://wik_x/path.md?rev=<sha>#h=...`) are returned alongside page-level
29
+ * ones — the server matches `prefix` OR `prefix?%` OR `prefix#%`. Everything
30
+ * else (task/message targets, wiki roots, changesets, and wiki URIs that carry
31
+ * an explicit `?rev`/`#` anchor) keeps exact `target_uri` matching, as does
32
+ * `--exact`. Exported for tests.
33
+ */
34
+ export function buildListTarget(target, opts) {
35
+ const uri = normalizeTarget(target);
36
+ if (!opts?.exact && isWikiFileUriWithoutAnchor(uri)) {
37
+ return { target_prefix: uri };
38
+ }
39
+ return { target_uri: uri };
40
+ }
13
41
  /**
14
42
  * Generic, target-agnostic comment commands over `POST/GET/PATCH/DELETE
15
43
  * /comments`. Unlike `parall tasks comments` (task-only) and `parall wiki
@@ -23,7 +51,8 @@ export function registerCommentCommands(program) {
23
51
  .description('Manage comments on any prll:// target (task / wiki page / changeset / inline anchor)');
24
52
  comments
25
53
  .command('add')
26
- .description('Add a comment to a prll:// target')
54
+ .description('Add a comment to a prll:// target. Inline wiki anchors must pin a revision: ' +
55
+ 'prll://wik_x/path.md?rev=<full_commit_sha>#h=<anchor>')
27
56
  .requiredOption('--target <uri>', 'Target prll:// URI (task / wiki page / changeset / inline anchor)')
28
57
  .requiredOption('--body <text>', 'Comment body')
29
58
  .option('--parent <id>', 'Parent comment ID (task comments only — threaded reply)')
@@ -44,11 +73,16 @@ export function registerCommentCommands(program) {
44
73
  });
45
74
  comments
46
75
  .command('list')
47
- .description('List comments on a prll:// target')
76
+ .description('List comments on a prll:// target. For a wiki file URI without an anchor ' +
77
+ '(prll://wik_x/path.md), inline anchored comments on that file ' +
78
+ '(...path.md?rev=<sha>#h=...) are included — each row carries its own ' +
79
+ 'target_uri so anchored rows are distinguishable. Use --exact to list ' +
80
+ 'only comments whose target matches the URI exactly.')
48
81
  .requiredOption('--target <uri>', 'Target prll:// URI')
49
82
  .option('--limit <n>', 'Maximum number of comments to return', '50')
50
83
  .option('--cursor <cursor>', 'Pagination cursor')
51
84
  .option('--order <order>', 'Sort order (asc or desc)')
85
+ .option('--exact', 'Match the target URI exactly (exclude inline anchored comments)')
52
86
  .action(async (opts) => {
53
87
  try {
54
88
  const { client, orgId } = resolveCredentials();
@@ -58,7 +92,7 @@ export function registerCommentCommands(program) {
58
92
  order: opts.order,
59
93
  });
60
94
  const result = await client.getComments(orgId, {
61
- target_uri: normalizeTarget(opts.target),
95
+ ...buildListTarget(opts.target, { exact: opts.exact }),
62
96
  ...params,
63
97
  });
64
98
  printJson(result);
@@ -1 +1 @@
1
- {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAuOvD"}
1
+ {"version":3,"file":"messages.d.ts","sourceRoot":"","sources":["../../src/commands/messages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAmBzC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAsPvD"}
@@ -51,6 +51,21 @@ export function registerMessageCommands(program) {
51
51
  printError(err);
52
52
  }
53
53
  });
54
+ messages
55
+ .command('react')
56
+ .description('Toggle an emoji reaction on a message (add if absent, remove if present)')
57
+ .argument('<messageId>', 'Message ID')
58
+ .argument('<emoji>', 'Emoji to toggle (e.g. 👍)')
59
+ .action(async (messageId, emoji) => {
60
+ try {
61
+ const { client } = resolveCredentials();
62
+ const result = await client.toggleReaction(stripPrllScheme(messageId), emoji);
63
+ printJson(result);
64
+ }
65
+ catch (err) {
66
+ printError(err);
67
+ }
68
+ });
54
69
  messages
55
70
  .command('send')
56
71
  .description('Send a message to a chat (text, file, or both)')
@@ -1,4 +1,12 @@
1
- import { Command } from 'commander';
1
+ import type { Command } from 'commander';
2
+ /**
3
+ * Resolve cross-chat authority as explicit --from, then runtime trigger, then
4
+ * none. --strict forces none and cannot be combined with --from; non-message
5
+ * runtime triggers are never accepted as message authority.
6
+ */
7
+ export declare function buildResolveRefsOptions(explicitFrom: string | undefined, triggerMessageId: string | undefined, strict?: boolean): {
8
+ sourceMessageId: string;
9
+ } | undefined;
2
10
  /** Build the SDK ref-graph params from the positional URI + CLI flags. */
3
11
  export declare function buildRefGraphParams(uri: string, opts: {
4
12
  depth?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../../src/commands/refs.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAIpC,0EAA0E;AAC1E,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACvB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAOjC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,QA+DnD"}
1
+ {"version":3,"file":"refs.d.ts","sourceRoot":"","sources":["../../src/commands/refs.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUzC;;;;GAIG;AACH,wBAAgB,uBAAuB,CACrC,YAAY,EAAE,MAAM,GAAG,SAAS,EAChC,gBAAgB,EAAE,MAAM,GAAG,SAAS,EACpC,MAAM,UAAQ,GACb;IAAE,eAAe,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,CAqBzC;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,MAAM,EACX,IAAI,EAAE;IAAE,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,GACvB;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAOjC;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,QAsEnD"}
@@ -1,5 +1,33 @@
1
- import { resolveCredentials } from '../lib/client.js';
2
- import { ensurePrllScheme, parsePositiveInt, printError, printJson } from '../lib/output.js';
1
+ import { resolveCredentials, resolveRuntimeContext } from '../lib/client.js';
2
+ import { ensurePrllScheme, parsePositiveInt, printError, printJson, stripPrllScheme, } from '../lib/output.js';
3
+ /**
4
+ * Resolve cross-chat authority as explicit --from, then runtime trigger, then
5
+ * none. --strict forces none and cannot be combined with --from; non-message
6
+ * runtime triggers are never accepted as message authority.
7
+ */
8
+ export function buildResolveRefsOptions(explicitFrom, triggerMessageId, strict = false) {
9
+ const explicit = explicitFrom?.trim();
10
+ if (strict && explicit) {
11
+ throw new Error('--strict cannot be combined with --from');
12
+ }
13
+ if (strict)
14
+ return undefined;
15
+ if (explicit) {
16
+ const sourceMessageId = stripPrllScheme(explicit);
17
+ if (!sourceMessageId.startsWith('msg_')) {
18
+ throw new Error('--from must be a message ID (msg_...) or prll://msg_... URI');
19
+ }
20
+ return { sourceMessageId };
21
+ }
22
+ // Typed task dispatches can carry a task ID in trigger_message_id. Only a
23
+ // real message can grant the server's cross-chat reference access; omitting
24
+ // non-message triggers preserves the normal membership-filtered resolve.
25
+ const trigger = triggerMessageId?.trim();
26
+ if (!trigger)
27
+ return undefined;
28
+ const sourceMessageId = stripPrllScheme(trigger);
29
+ return sourceMessageId.startsWith('msg_') ? { sourceMessageId } : undefined;
30
+ }
3
31
  /** Build the SDK ref-graph params from the positional URI + CLI flags. */
4
32
  export function buildRefGraphParams(uri, opts) {
5
33
  const params = { uri: ensurePrllScheme(uri) };
@@ -15,10 +43,13 @@ export function registerRefCommands(program) {
15
43
  refs
16
44
  .command('resolve <uris...>')
17
45
  .description('Batch resolve prll:// URIs to entity metadata')
18
- .action(async (uris) => {
46
+ .option('--from <messageId>', 'Forwarding message for cross-chat access (defaults to trigger)')
47
+ .option('--strict', 'Ignore runtime trigger and enforce direct membership checks')
48
+ .action(async (uris, opts) => {
19
49
  try {
20
50
  const { client, orgId } = resolveCredentials();
21
- const result = await client.resolveRefs(orgId, uris);
51
+ const runtime = resolveRuntimeContext();
52
+ const result = await client.resolveRefs(orgId, uris, buildResolveRefsOptions(opts.from, runtime.triggerMessageId, opts.strict));
22
53
  printJson(result);
23
54
  }
24
55
  catch (err) {
@@ -10,6 +10,7 @@ export declare function buildCreateTaskData(opts: {
10
10
  parentId?: string;
11
11
  sourceChatId?: string;
12
12
  dueDate?: string;
13
+ plannedStart?: string;
13
14
  }): Partial<{
14
15
  title: string;
15
16
  description: string | undefined;
@@ -19,6 +20,7 @@ export declare function buildCreateTaskData(opts: {
19
20
  project_id: string | undefined;
20
21
  parent_id: string | undefined;
21
22
  source_chat_id: string | undefined;
23
+ planned_start_date: string | undefined;
22
24
  due_date: string | undefined;
23
25
  }>;
24
26
  export declare function buildUpdateTaskData(opts: {
@@ -32,6 +34,7 @@ export declare function buildUpdateTaskData(opts: {
32
34
  sortOrder?: string;
33
35
  placement?: string;
34
36
  dueDate?: string;
37
+ plannedStart?: string;
35
38
  }): Partial<{
36
39
  title: string | undefined;
37
40
  status: UpdateTaskRequest["status"];
@@ -42,6 +45,7 @@ export declare function buildUpdateTaskData(opts: {
42
45
  parent_id: string | undefined;
43
46
  sort_order: number | undefined;
44
47
  placement: "end" | undefined;
48
+ planned_start_date: string | null | undefined;
45
49
  due_date: string | null | undefined;
46
50
  }>;
47
51
  export declare function registerTaskCommands(program: Command): void;
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AA8DpC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;;;;;;;;;;GAYA;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;;YAS0B,iBAAiB,CAAC,QAAQ,CAAC;cACvB,iBAAiB,CAAC,UAAU,CAAC;;;;;;;;GAS3D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAiWpD"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgEpC,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;;;;;;;;;;GAaA;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;;YAS0B,iBAAiB,CAAC,QAAQ,CAAC;cACvB,iBAAiB,CAAC,UAAU,CAAC;;;;;;;;;GAU3D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAyWpD"}
@@ -10,26 +10,28 @@ function stripUndefined(obj) {
10
10
  // intent into a successful no-op. The server independently re-validates and
11
11
  // stays authoritative for domain semantics (calendar validity, placement
12
12
  // set, ordering conflicts, permissions).
13
- /** Strict command grammar for --due-date; calendar validity (e.g. 2026-02-30)
14
- * stays the server's domain (400 INVALID_DUE_DATE). */
15
- const DUE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
16
- function resolveCreateDueDate(value) {
13
+ /** Strict command grammar for the date flags (--due-date, --planned-start);
14
+ * calendar validity (e.g. 2026-02-30) and the cross-field ordering rule
15
+ * (planned start <= due) stay the server's domain (400 INVALID_DUE_DATE /
16
+ * INVALID_PLANNED_START_DATE / INVALID_DATE_RANGE). */
17
+ const DATE_FLAG_RE = /^\d{4}-\d{2}-\d{2}$/;
18
+ function resolveCreateDateFlag(flag, value) {
17
19
  if (value === undefined)
18
20
  return undefined;
19
- if (!DUE_DATE_RE.test(value)) {
20
- throw new Error('--due-date must be a YYYY-MM-DD date');
21
+ if (!DATE_FLAG_RE.test(value)) {
22
+ throw new Error(`${flag} must be a YYYY-MM-DD date`);
21
23
  }
22
24
  return value;
23
25
  }
24
26
  /** Update grammar adds the lowercase "none" sentinel — the agent-facing
25
27
  * clear intent, mapped to an explicit null on the wire. */
26
- function resolveUpdateDueDate(value) {
28
+ function resolveUpdateDateFlag(flag, value) {
27
29
  if (value === undefined)
28
30
  return undefined;
29
31
  if (value === 'none')
30
32
  return null;
31
- if (!DUE_DATE_RE.test(value)) {
32
- throw new Error('--due-date must be a YYYY-MM-DD date, or "none" to clear it');
33
+ if (!DATE_FLAG_RE.test(value)) {
34
+ throw new Error(`${flag} must be a YYYY-MM-DD date, or "none" to clear it`);
33
35
  }
34
36
  return value;
35
37
  }
@@ -67,7 +69,8 @@ export function buildCreateTaskData(opts) {
67
69
  project_id: opts.projectId ? stripPrllScheme(opts.projectId) : undefined,
68
70
  parent_id: opts.parentId ? stripPrllScheme(opts.parentId) : undefined,
69
71
  source_chat_id: opts.sourceChatId ? stripPrllScheme(opts.sourceChatId) : undefined,
70
- due_date: resolveCreateDueDate(opts.dueDate),
72
+ planned_start_date: resolveCreateDateFlag('--planned-start', opts.plannedStart),
73
+ due_date: resolveCreateDateFlag('--due-date', opts.dueDate),
71
74
  });
72
75
  }
73
76
  export function buildUpdateTaskData(opts) {
@@ -87,7 +90,8 @@ export function buildUpdateTaskData(opts) {
87
90
  parent_id: opts.parentId ? stripPrllScheme(opts.parentId) : undefined,
88
91
  sort_order: resolveSortOrder(opts.sortOrder),
89
92
  placement: resolvePlacement(opts.placement),
90
- due_date: resolveUpdateDueDate(opts.dueDate),
93
+ planned_start_date: resolveUpdateDateFlag('--planned-start', opts.plannedStart),
94
+ due_date: resolveUpdateDateFlag('--due-date', opts.dueDate),
91
95
  });
92
96
  }
93
97
  export function registerTaskCommands(program) {
@@ -136,6 +140,7 @@ export function registerTaskCommands(program) {
136
140
  .option('--project-id <id>', 'Project ID')
137
141
  .option('--parent-id <id>', 'Parent task ID')
138
142
  .option('--source-chat-id <id>', 'Source chat ID')
143
+ .option('--planned-start <date>', 'Planned start date (YYYY-MM-DD) — must be on or before --due-date when both are set')
139
144
  .option('--due-date <date>', 'Planned completion date (YYYY-MM-DD)')
140
145
  .action(async (opts) => {
141
146
  try {
@@ -178,6 +183,7 @@ export function registerTaskCommands(program) {
178
183
  .option('--parent-id <id>', 'Parent task ID')
179
184
  .option('--sort-order <n>', 'Explicit sort_order value — only for pinpoint insertion relative to cards you can see; to append to a status column use --placement end')
180
185
  .option('--placement <where>', 'Ordering intent — only "end" is accepted: appends the task to the end of its status column, position resolved by the server (mutually exclusive with --sort-order)')
186
+ .option('--planned-start <date>', 'Planned start date (YYYY-MM-DD), or "none" to clear it — must be on or before the due date when both are set')
181
187
  .option('--due-date <date>', 'Planned completion date (YYYY-MM-DD), or "none" to clear it')
182
188
  .action(async (taskId, opts) => {
183
189
  try {
@@ -0,0 +1,55 @@
1
+ import type { EdgeClipExecResult, ExecEdgeClipRequest, ParallClient } from '@parall/sdk';
2
+ /**
3
+ * Cloud (hosted) Edge activation wait — CLI-side bounded retry for `clip exec`.
4
+ *
5
+ * A clip connection bound to a `placement=hosted` Edge whose pod is cold returns
6
+ * 503 EDGE_ACTIVATING from the exec pre-flight: the platform is starting the
7
+ * pod, and the command has NOT been dispatched (the server answers before any
8
+ * publish), so retrying the identical request cannot double-execute anything.
9
+ * This module absorbs that one precise error so a single `parall clip exec`
10
+ * rides through cold start instead of forcing the caller (agent or human) into
11
+ * a blind retry loop.
12
+ *
13
+ * Everything else propagates on first occurrence — especially OUTCOME_UNKNOWN
14
+ * (504): the command was dispatched and MAY HAVE EXECUTED; an automatic retry
15
+ * could post, order or delete twice. That decision belongs to the caller, with
16
+ * the request id from the error message, never to a loop in here.
17
+ *
18
+ * Deliberately NOT in the SDK: `execEdgeClip()` keeps its throw-on-503
19
+ * semantics for every other consumer; only the CLI exec behavior waits.
20
+ * Mirrors lib/clip-invoke.ts (the v2 hosted-browser wait) — same budget, same
21
+ * schedule, same exact-code matching discipline.
22
+ */
23
+ /** Total activation wait budget. Independent of the clip command --timeout:
24
+ * each attempt keeps its own full command timeout, so worst-case wall clock is
25
+ * roughly this budget plus one command timeout. */
26
+ export declare const EDGE_ACTIVATION_WAIT_BUDGET_MS = 60000;
27
+ /** Backoff schedule between activation retries; the last entry repeats. */
28
+ export declare const EDGE_ACTIVATION_BACKOFF_MS: number[];
29
+ /** Injectable time hooks so tests never really sleep. */
30
+ export type EdgeActivationWaitHooks = {
31
+ sleep?: (ms: number) => Promise<void>;
32
+ now?: () => number;
33
+ };
34
+ /**
35
+ * The ONLY retryable error: a typed ApiError carrying exactly 503 +
36
+ * EDGE_ACTIVATING. Everything else — routing errors, EDGE_BUSY, capacity,
37
+ * repair, command failures, timeouts, OUTCOME_UNKNOWN, other 5xx — means
38
+ * retrying is either pointless or unsafe, so it propagates unchanged on the
39
+ * first occurrence.
40
+ */
41
+ export declare function isEdgeActivationPending(err: unknown): boolean;
42
+ /**
43
+ * Execute an Edge clip, transparently absorbing the cloud-profile activation
44
+ * window.
45
+ *
46
+ * Retries the IDENTICAL request while the server reports EDGE_ACTIVATING,
47
+ * backing off 500ms → 1s → 2s (capped) until the 60s activation budget is
48
+ * spent. The whole loop carries one `correlation_id` (the caller's, or one
49
+ * minted here) so the server's exec audit reads the retries as a single
50
+ * logical call. Once the budget is spent, the LAST typed ApiError is rethrown
51
+ * as-is — status/code intact — so the printed error stays machine-readable.
52
+ * Any other failure is rethrown immediately.
53
+ */
54
+ export declare function execEdgeClipAwaitingActivation(client: Pick<ParallClient, 'execEdgeClip'>, orgId: string, req: ExecEdgeClipRequest, hooks?: EdgeActivationWaitHooks): Promise<EdgeClipExecResult>;
55
+ //# sourceMappingURL=edge-exec.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"edge-exec.d.ts","sourceRoot":"","sources":["../../src/lib/edge-exec.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAGzF;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH;;oDAEoD;AACpD,eAAO,MAAM,8BAA8B,QAAS,CAAC;AAErD,2EAA2E;AAC3E,eAAO,MAAM,0BAA0B,UAAsB,CAAC;AAE9D,yDAAyD;AACzD,MAAM,MAAM,uBAAuB,GAAG;IACpC,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,uBAAuB,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAE7D;AAED;;;;;;;;;;;GAWG;AACH,wBAAsB,8BAA8B,CAClD,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,cAAc,CAAC,EAC1C,KAAK,EAAE,MAAM,EACb,GAAG,EAAE,mBAAmB,EACxB,KAAK,CAAC,EAAE,uBAAuB,GAC9B,OAAO,CAAC,kBAAkB,CAAC,CAuB7B"}
@@ -0,0 +1,76 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { ApiError } from '@parall/sdk';
3
+ /**
4
+ * Cloud (hosted) Edge activation wait — CLI-side bounded retry for `clip exec`.
5
+ *
6
+ * A clip connection bound to a `placement=hosted` Edge whose pod is cold returns
7
+ * 503 EDGE_ACTIVATING from the exec pre-flight: the platform is starting the
8
+ * pod, and the command has NOT been dispatched (the server answers before any
9
+ * publish), so retrying the identical request cannot double-execute anything.
10
+ * This module absorbs that one precise error so a single `parall clip exec`
11
+ * rides through cold start instead of forcing the caller (agent or human) into
12
+ * a blind retry loop.
13
+ *
14
+ * Everything else propagates on first occurrence — especially OUTCOME_UNKNOWN
15
+ * (504): the command was dispatched and MAY HAVE EXECUTED; an automatic retry
16
+ * could post, order or delete twice. That decision belongs to the caller, with
17
+ * the request id from the error message, never to a loop in here.
18
+ *
19
+ * Deliberately NOT in the SDK: `execEdgeClip()` keeps its throw-on-503
20
+ * semantics for every other consumer; only the CLI exec behavior waits.
21
+ * Mirrors lib/clip-invoke.ts (the v2 hosted-browser wait) — same budget, same
22
+ * schedule, same exact-code matching discipline.
23
+ */
24
+ /** Total activation wait budget. Independent of the clip command --timeout:
25
+ * each attempt keeps its own full command timeout, so worst-case wall clock is
26
+ * roughly this budget plus one command timeout. */
27
+ export const EDGE_ACTIVATION_WAIT_BUDGET_MS = 60_000;
28
+ /** Backoff schedule between activation retries; the last entry repeats. */
29
+ export const EDGE_ACTIVATION_BACKOFF_MS = [500, 1_000, 2_000];
30
+ const realSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
31
+ /**
32
+ * The ONLY retryable error: a typed ApiError carrying exactly 503 +
33
+ * EDGE_ACTIVATING. Everything else — routing errors, EDGE_BUSY, capacity,
34
+ * repair, command failures, timeouts, OUTCOME_UNKNOWN, other 5xx — means
35
+ * retrying is either pointless or unsafe, so it propagates unchanged on the
36
+ * first occurrence.
37
+ */
38
+ export function isEdgeActivationPending(err) {
39
+ return err instanceof ApiError && err.status === 503 && err.code === 'EDGE_ACTIVATING';
40
+ }
41
+ /**
42
+ * Execute an Edge clip, transparently absorbing the cloud-profile activation
43
+ * window.
44
+ *
45
+ * Retries the IDENTICAL request while the server reports EDGE_ACTIVATING,
46
+ * backing off 500ms → 1s → 2s (capped) until the 60s activation budget is
47
+ * spent. The whole loop carries one `correlation_id` (the caller's, or one
48
+ * minted here) so the server's exec audit reads the retries as a single
49
+ * logical call. Once the budget is spent, the LAST typed ApiError is rethrown
50
+ * as-is — status/code intact — so the printed error stays machine-readable.
51
+ * Any other failure is rethrown immediately.
52
+ */
53
+ export async function execEdgeClipAwaitingActivation(client, orgId, req, hooks) {
54
+ const sleep = hooks?.sleep ?? realSleep;
55
+ const now = hooks?.now ?? Date.now;
56
+ const pinned = {
57
+ ...req,
58
+ correlation_id: req.correlation_id ?? randomUUID(),
59
+ };
60
+ const deadline = now() + EDGE_ACTIVATION_WAIT_BUDGET_MS;
61
+ for (let attempt = 0;; attempt++) {
62
+ try {
63
+ return await client.execEdgeClip(orgId, pinned);
64
+ }
65
+ catch (err) {
66
+ if (!isEdgeActivationPending(err))
67
+ throw err;
68
+ const remaining = deadline - now();
69
+ if (remaining <= 0)
70
+ throw err;
71
+ const backoff = EDGE_ACTIVATION_BACKOFF_MS[Math.min(attempt, EDGE_ACTIVATION_BACKOFF_MS.length - 1)];
72
+ // Clamp to the remaining budget so the final sleep can't overrun it.
73
+ await sleep(Math.min(backoff, remaining));
74
+ }
75
+ }
76
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/cli",
3
- "version": "1.47.0",
3
+ "version": "1.49.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.47.0",
40
- "@parall/sdk": "1.47.0"
39
+ "@parall/agent-core": "1.49.0",
40
+ "@parall/sdk": "1.49.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.47.0"
46
+ "@parall/agent-core": "1.49.0"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc",