@parall/cli 1.55.3 → 1.55.4

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.
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerProfileCommands(program: Command): void;
3
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../../src/commands/profile.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAWpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QA8GvD"}
@@ -0,0 +1,114 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolveCredentials } from '../lib/client.js';
3
+ import { printJson, printError } from '../lib/output.js';
4
+ // Profile commands follow the platform's uniform permission loop: `set` calls
5
+ // the ATOMIC endpoints directly. Callers with edit power (humans on personal
6
+ // keys) mutate immediately; a denied caller (an agent editing itself) gets the
7
+ // structured 403 whose printed `Request approval:` command carries the exact
8
+ // payload that was attempted — running it files the proposal for a human to
9
+ // decide. There is no separate "propose" verb.
10
+ export function registerProfileCommands(program) {
11
+ const profile = program.command('profile').description('Read and edit your profile');
12
+ profile
13
+ .command('show')
14
+ .description('Show your identity, org profile, Instructions and manager')
15
+ .action(async () => {
16
+ try {
17
+ const { client, orgId } = resolveCredentials();
18
+ const me = await client.getMe();
19
+ const result = { user: me };
20
+ try {
21
+ result.org_profile = await client.getMemberProfile(orgId, me.id);
22
+ }
23
+ catch {
24
+ /* display-only enrichment */
25
+ }
26
+ if (me.type === 'agent') {
27
+ try {
28
+ result.instructions = await client.getAgentInstructions(orgId, me.id);
29
+ }
30
+ catch {
31
+ /* not readable */
32
+ }
33
+ try {
34
+ result.manager = await client.getAgentManager(orgId, me.id);
35
+ }
36
+ catch {
37
+ /* not readable */
38
+ }
39
+ }
40
+ printJson(result);
41
+ }
42
+ catch (err) {
43
+ printError(err);
44
+ }
45
+ });
46
+ profile
47
+ .command('set')
48
+ .description('Edit your profile (title/about, Instructions, display name). If you lack permission, the error prints a ready-made approval request carrying exactly what you tried to write.')
49
+ .option('--title <text>', 'Org-profile title')
50
+ .option('--about <text>', 'Org-profile about/description')
51
+ .option('--instructions <text>', 'Full replacement Instructions')
52
+ .option('--instructions-file <path>', 'Read replacement Instructions from a file')
53
+ .option('--display-name <name>', 'Display name')
54
+ .action(async (opts) => {
55
+ if (opts.instructions !== undefined && opts.instructionsFile !== undefined) {
56
+ printError(new Error('--instructions and --instructions-file are mutually exclusive'));
57
+ }
58
+ const wantsProfile = opts.title !== undefined || opts.about !== undefined;
59
+ const instructionsBody = opts.instructions ??
60
+ (opts.instructionsFile ? readFileSync(opts.instructionsFile, 'utf8') : undefined);
61
+ if (!wantsProfile && instructionsBody === undefined && opts.displayName === undefined) {
62
+ printError(new Error('Nothing to change. Provide --title/--about, --instructions(-file), or --display-name.'));
63
+ }
64
+ const { client, orgId } = resolveCredentials();
65
+ const me = await client.getMe().catch((err) => printError(err));
66
+ const applied = {};
67
+ // Field groups apply independently, in order; the first denial exits
68
+ // with the prefilled proposal command for THAT group.
69
+ if (wantsProfile) {
70
+ // Read-modify-write like any client: a missing profile row is a
71
+ // successful empty read at version 0. Unspecified fields keep their
72
+ // current value so the write (and any resulting proposal) is always
73
+ // the full final state.
74
+ const current = await client.getMemberProfile(orgId, me.id).catch((err) => printError(err));
75
+ const body = {
76
+ title: opts.title ?? current.title,
77
+ description: opts.about ?? current.description,
78
+ expected_version: current.profile_version,
79
+ };
80
+ try {
81
+ applied.org_profile = await client.updateMemberProfile(orgId, me.id, body);
82
+ }
83
+ catch (err) {
84
+ printError(err, body);
85
+ }
86
+ }
87
+ if (instructionsBody !== undefined) {
88
+ const current = await client
89
+ .getAgentInstructions(orgId, me.id)
90
+ .catch((err) => printError(err));
91
+ const body = { instructions: instructionsBody, expected_version: current.version };
92
+ try {
93
+ applied.instructions = await client.updateAgentInstructions(orgId, me.id, body);
94
+ }
95
+ catch (err) {
96
+ printError(err, body);
97
+ }
98
+ }
99
+ if (opts.displayName !== undefined) {
100
+ try {
101
+ applied.user = await client.updateMe({ display_name: opts.displayName });
102
+ }
103
+ catch (err) {
104
+ // The identity proposal needs the CAS base (name at filing time),
105
+ // which the direct PATCH body does not carry — prefill it here.
106
+ printError(err, {
107
+ display_name: opts.displayName,
108
+ previous_display_name: me.display_name,
109
+ });
110
+ }
111
+ }
112
+ printJson(applied);
113
+ });
114
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/commands/projects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QA8FvD"}
1
+ {"version":3,"file":"projects.d.ts","sourceRoot":"","sources":["../../src/commands/projects.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,QAyOvD"}
@@ -1,5 +1,6 @@
1
1
  import { resolveCredentials } from '../lib/client.js';
2
2
  import { printJson, printError, stripPrllScheme } from '../lib/output.js';
3
+ const VISIBILITY_HELP = 'public (any org member self-joins), restricted (join by approved request), or private (invite only); reading requires roster membership, with org owners/admins implicit';
3
4
  export function registerProjectCommands(program) {
4
5
  const projects = program.command('projects').description('Manage projects');
5
6
  projects
@@ -35,7 +36,7 @@ export function registerProjectCommands(program) {
35
36
  .requiredOption('--name <name>', 'Project name')
36
37
  .requiredOption('--key <key>', 'Project key')
37
38
  .option('--description <text>', 'Project description')
38
- .option('--visibility <visibility>', 'public (whole org) or private (roster only)')
39
+ .option('--visibility <visibility>', VISIBILITY_HELP)
39
40
  .option('--color <hex>', 'Project color (hex)')
40
41
  .action(async (opts) => {
41
42
  try {
@@ -60,7 +61,7 @@ export function registerProjectCommands(program) {
60
61
  .argument('<projectId>', 'Project ID')
61
62
  .option('--name <name>', 'Project name')
62
63
  .option('--description <text>', 'Project description')
63
- .option('--visibility <visibility>', 'public (whole org) or private (roster only)')
64
+ .option('--visibility <visibility>', VISIBILITY_HELP)
64
65
  .option('--color <hex>', 'Project color (hex)')
65
66
  .option('--status <status>', 'Project status')
66
67
  .action(async (projectId, opts) => {
@@ -102,4 +103,133 @@ export function registerProjectCommands(program) {
102
103
  printError(err);
103
104
  }
104
105
  });
106
+ projects
107
+ .command('library')
108
+ .description('Browse joinable projects (public + restricted) with your admission state. If task creation fails with no available projects, start here.')
109
+ .action(async () => {
110
+ try {
111
+ const { client, orgId } = resolveCredentials();
112
+ const result = await client.getProjectLibrary(orgId);
113
+ printJson(result);
114
+ }
115
+ catch (err) {
116
+ printError(err);
117
+ }
118
+ });
119
+ projects
120
+ .command('join')
121
+ .description('Join a public project immediately (restricted projects need `projects request-join`)')
122
+ .argument('<projectId>', 'Project ID')
123
+ .action(async (projectId) => {
124
+ try {
125
+ const { client, orgId } = resolveCredentials();
126
+ const result = await client.joinProject(orgId, stripPrllScheme(projectId));
127
+ printJson(result);
128
+ }
129
+ catch (err) {
130
+ printError(err);
131
+ }
132
+ });
133
+ projects
134
+ .command('request-join')
135
+ .description('File a join request for a restricted project; a project manager approves it')
136
+ .argument('<projectId>', 'Project ID')
137
+ .option('--cancel', 'Withdraw your pending join request instead')
138
+ .action(async (projectId, opts) => {
139
+ try {
140
+ const { client, orgId } = resolveCredentials();
141
+ const id = stripPrllScheme(projectId);
142
+ if (opts.cancel) {
143
+ await client.cancelProjectJoinRequest(orgId, id);
144
+ printJson({ cancelled: id });
145
+ return;
146
+ }
147
+ const result = await client.createProjectJoinRequest(orgId, id);
148
+ printJson(result);
149
+ }
150
+ catch (err) {
151
+ printError(err);
152
+ }
153
+ });
154
+ projects
155
+ .command('join-requests')
156
+ .description('List pending join requests for a project (manager standing required)')
157
+ .argument('<projectId>', 'Project ID')
158
+ .action(async (projectId) => {
159
+ try {
160
+ const { client, orgId } = resolveCredentials();
161
+ const result = await client.listProjectJoinRequests(orgId, stripPrllScheme(projectId));
162
+ printJson(result);
163
+ }
164
+ catch (err) {
165
+ printError(err);
166
+ }
167
+ });
168
+ const members = projects.command('members').description('Manage a project roster');
169
+ members
170
+ .command('list')
171
+ .description('List roster members (readable by anyone who can read the project)')
172
+ .argument('<projectId>', 'Project ID')
173
+ .action(async (projectId) => {
174
+ try {
175
+ const { client, orgId } = resolveCredentials();
176
+ const result = await client.getProjectMembers(orgId, stripPrllScheme(projectId));
177
+ printJson(result);
178
+ }
179
+ catch (err) {
180
+ printError(err);
181
+ }
182
+ });
183
+ members
184
+ .command('add')
185
+ .description('Add a user or team to the roster (manager standing required)')
186
+ .argument('<projectId>', 'Project ID')
187
+ .argument('<subject>', 'User ID (usr_...) or team ID (team_...)')
188
+ .option('--role <role>', 'member or manager', 'member')
189
+ .action(async (projectId, subject, opts) => {
190
+ try {
191
+ const { client, orgId } = resolveCredentials();
192
+ const result = await client.addProjectMember(orgId, stripPrllScheme(projectId), {
193
+ subject: stripPrllScheme(subject),
194
+ role: opts.role,
195
+ });
196
+ printJson(result);
197
+ }
198
+ catch (err) {
199
+ printError(err);
200
+ }
201
+ });
202
+ members
203
+ .command('remove')
204
+ .description('Remove a roster entry (manager standing required)')
205
+ .argument('<projectId>', 'Project ID')
206
+ .argument('<subject>', 'User ID or team ID')
207
+ .action(async (projectId, subject) => {
208
+ try {
209
+ const { client, orgId } = resolveCredentials();
210
+ const id = stripPrllScheme(projectId);
211
+ const sub = stripPrllScheme(subject);
212
+ await client.removeProjectMember(orgId, id, sub);
213
+ printJson({ removed: sub, project: id });
214
+ }
215
+ catch (err) {
216
+ printError(err);
217
+ }
218
+ });
219
+ members
220
+ .command('set-role')
221
+ .description('Change a roster entry role (manager standing required)')
222
+ .argument('<projectId>', 'Project ID')
223
+ .argument('<subject>', 'User ID or team ID')
224
+ .requiredOption('--role <role>', 'member or manager')
225
+ .action(async (projectId, subject, opts) => {
226
+ try {
227
+ const { client, orgId } = resolveCredentials();
228
+ const result = await client.updateProjectMember(orgId, stripPrllScheme(projectId), stripPrllScheme(subject), { role: opts.role });
229
+ printJson(result);
230
+ }
231
+ catch (err) {
232
+ printError(err);
233
+ }
234
+ });
105
235
  }
@@ -4,8 +4,9 @@ import type { Command } from 'commander';
4
4
  * none. --strict forces none and cannot be combined with --from; non-message
5
5
  * runtime triggers are never accepted as message authority.
6
6
  */
7
- export declare function buildResolveRefsOptions(explicitFrom: string | undefined, triggerMessageId: string | undefined, strict?: boolean): {
8
- sourceMessageId: string;
7
+ export declare function buildResolveRefsOptions(explicitFrom: string | undefined, triggerMessageId: string | undefined, strict?: boolean, full?: boolean): {
8
+ sourceMessageId?: string;
9
+ full?: boolean;
9
10
  } | undefined;
10
11
  /** Build the SDK ref-graph params from the positional URI + CLI flags. */
11
12
  export declare function buildRefGraphParams(uri: string, opts: {
@@ -1 +1 @@
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
+ {"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,EACd,IAAI,UAAQ,GACX;IAAE,eAAe,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAsB1D;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,QA0EnD"}
@@ -5,28 +5,30 @@ import { ensurePrllScheme, parsePositiveInt, printError, printJson, stripPrllSch
5
5
  * none. --strict forces none and cannot be combined with --from; non-message
6
6
  * runtime triggers are never accepted as message authority.
7
7
  */
8
- export function buildResolveRefsOptions(explicitFrom, triggerMessageId, strict = false) {
8
+ export function buildResolveRefsOptions(explicitFrom, triggerMessageId, strict = false, full = false) {
9
9
  const explicit = explicitFrom?.trim();
10
10
  if (strict && explicit) {
11
11
  throw new Error('--strict cannot be combined with --from');
12
12
  }
13
13
  if (strict)
14
- return undefined;
14
+ return full ? { full: true } : undefined;
15
15
  if (explicit) {
16
16
  const sourceMessageId = stripPrllScheme(explicit);
17
17
  if (!sourceMessageId.startsWith('msg_')) {
18
18
  throw new Error('--from must be a message ID (msg_...) or prll://msg_... URI');
19
19
  }
20
- return { sourceMessageId };
20
+ return full ? { sourceMessageId, full: true } : { sourceMessageId };
21
21
  }
22
22
  // Typed task dispatches can carry a task ID in trigger_message_id. Only a
23
23
  // real message can grant the server's cross-chat reference access; omitting
24
24
  // non-message triggers preserves the normal membership-filtered resolve.
25
25
  const trigger = triggerMessageId?.trim();
26
26
  if (!trigger)
27
- return undefined;
27
+ return full ? { full: true } : undefined;
28
28
  const sourceMessageId = stripPrllScheme(trigger);
29
- return sourceMessageId.startsWith('msg_') ? { sourceMessageId } : undefined;
29
+ if (!sourceMessageId.startsWith('msg_'))
30
+ return full ? { full: true } : undefined;
31
+ return full ? { sourceMessageId, full: true } : { sourceMessageId };
30
32
  }
31
33
  /** Build the SDK ref-graph params from the positional URI + CLI flags. */
32
34
  export function buildRefGraphParams(uri, opts) {
@@ -45,11 +47,15 @@ export function registerRefCommands(program) {
45
47
  .description('Batch resolve prll:// URIs to entity metadata')
46
48
  .option('--from <messageId>', 'Forwarding message for cross-chat access (defaults to trigger)')
47
49
  .option('--strict', 'Ignore runtime trigger and enforce direct membership checks')
50
+ .option('--full', 'Return complete text for authorized message references')
48
51
  .action(async (uris, opts) => {
49
52
  try {
50
53
  const { client, orgId } = resolveCredentials();
51
54
  const runtime = resolveRuntimeContext();
52
- const result = await client.resolveRefs(orgId, uris, buildResolveRefsOptions(opts.from, runtime.triggerMessageId, opts.strict));
55
+ // Normalize schemeless args (msg_x, cht_x#range=…) like `refs graph`
56
+ // does: the server drops non-prll URIs without an error, so a raw id
57
+ // would silently resolve to nothing.
58
+ const result = await client.resolveRefs(orgId, uris.map(ensurePrllScheme), buildResolveRefsOptions(opts.from, runtime.triggerMessageId, opts.strict, opts.full));
53
59
  printJson(result);
54
60
  }
55
61
  catch (err) {
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACxE,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,EAAE,MAAM,CAAC;IAClB,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,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;;;;;;;;GAS3D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QA2WpD"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAqB,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACxE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAiEpC,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,EAAE,MAAM,CAAC;IAClB,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,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;;;;;;;;GAS3D;AAED,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAkXpD"}
@@ -1,4 +1,5 @@
1
1
  import { ApiError } from '@parall/sdk';
2
+ import { createHash } from 'node:crypto';
2
3
  import { resolveCredentials, resolveTypedDispatchBinding } from '../lib/client.js';
3
4
  import { printJson, printError, printRefHint, stripPrllScheme } from '../lib/output.js';
4
5
  function stripUndefined(obj) {
@@ -201,10 +202,18 @@ export function registerTaskCommands(program) {
201
202
  const binding = resolveTypedDispatchBinding(taskId);
202
203
  const req = { ...data };
203
204
  if (binding) {
204
- req.dispatch_lane = binding.lane;
205
+ if (binding.lane)
206
+ req.dispatch_lane = binding.lane;
205
207
  req.dispatch_event_id = binding.dispatchEventId;
206
- if (binding.effectKey)
207
- req.dispatch_effect_key = binding.effectKey;
208
+ if (binding.generation !== undefined)
209
+ req.dispatch_generation = binding.generation;
210
+ if (binding.effectKey) {
211
+ req.dispatch_effect_key = binding.lane
212
+ ? binding.effectKey
213
+ : // Converged: content-address the key so a same-payload retry
214
+ // replays idempotently and a different update mints a new key.
215
+ `${binding.effectKey}:${createHash('sha256').update(JSON.stringify(data)).digest('hex').slice(0, 12)}`;
216
+ }
208
217
  }
209
218
  const result = await client.updateTask(orgId, taskId, req);
210
219
  if (binding?.effectKey) {
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ export declare function registerTeamCommands(program: Command): void;
3
+ //# sourceMappingURL=teams.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"teams.d.ts","sourceRoot":"","sources":["../../src/commands/teams.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAOpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAiGpD"}
@@ -0,0 +1,102 @@
1
+ import { resolveCredentials } from '../lib/client.js';
2
+ import { printJson, printError, stripPrllScheme } from '../lib/output.js';
3
+ // Teams are pure permission rosters (ACL subjects for wiki/project ACLs);
4
+ // server-side authorization decides who may mutate them — the CLI only
5
+ // exposes the same surface the web Teams settings use.
6
+ export function registerTeamCommands(program) {
7
+ const teams = program.command('teams').description('Manage permission teams');
8
+ teams
9
+ .command('list')
10
+ .description('List teams in the organization')
11
+ .action(async () => {
12
+ try {
13
+ const { client, orgId } = resolveCredentials();
14
+ const result = await client.getTeams(orgId);
15
+ printJson(result);
16
+ }
17
+ catch (err) {
18
+ printError(err);
19
+ }
20
+ });
21
+ teams
22
+ .command('get')
23
+ .description('Get a team by ID')
24
+ .argument('<teamId>', 'Team ID')
25
+ .action(async (teamId) => {
26
+ try {
27
+ const { client, orgId } = resolveCredentials();
28
+ const result = await client.getTeam(orgId, stripPrllScheme(teamId));
29
+ printJson(result);
30
+ }
31
+ catch (err) {
32
+ printError(err);
33
+ }
34
+ });
35
+ const members = teams.command('members').description('Manage a team roster');
36
+ members
37
+ .command('list')
38
+ .description('List team members')
39
+ .argument('<teamId>', 'Team ID')
40
+ .action(async (teamId) => {
41
+ try {
42
+ const { client, orgId } = resolveCredentials();
43
+ const result = await client.getTeamMembers(orgId, stripPrllScheme(teamId));
44
+ printJson(result);
45
+ }
46
+ catch (err) {
47
+ printError(err);
48
+ }
49
+ });
50
+ members
51
+ .command('add')
52
+ .description('Add a member (team manager or org admin)')
53
+ .argument('<teamId>', 'Team ID')
54
+ .argument('<userId>', 'User ID (usr_...)')
55
+ .option('--role <role>', 'member or manager', 'member')
56
+ .action(async (teamId, userId, opts) => {
57
+ try {
58
+ const { client, orgId } = resolveCredentials();
59
+ await client.addTeamMember(orgId, stripPrllScheme(teamId), {
60
+ user_id: stripPrllScheme(userId),
61
+ role: opts.role,
62
+ });
63
+ printJson({ added: stripPrllScheme(userId), team: stripPrllScheme(teamId) });
64
+ }
65
+ catch (err) {
66
+ printError(err);
67
+ }
68
+ });
69
+ members
70
+ .command('remove')
71
+ .description('Remove a member (team manager or org admin)')
72
+ .argument('<teamId>', 'Team ID')
73
+ .argument('<userId>', 'User ID')
74
+ .action(async (teamId, userId) => {
75
+ try {
76
+ const { client, orgId } = resolveCredentials();
77
+ await client.removeTeamMember(orgId, stripPrllScheme(teamId), stripPrllScheme(userId));
78
+ printJson({ removed: stripPrllScheme(userId), team: stripPrllScheme(teamId) });
79
+ }
80
+ catch (err) {
81
+ printError(err);
82
+ }
83
+ });
84
+ members
85
+ .command('set-role')
86
+ .description('Change a member role (org admin; managers manage plain members only)')
87
+ .argument('<teamId>', 'Team ID')
88
+ .argument('<userId>', 'User ID')
89
+ .requiredOption('--role <role>', 'member or manager')
90
+ .action(async (teamId, userId, opts) => {
91
+ try {
92
+ const { client, orgId } = resolveCredentials();
93
+ await client.updateTeamMember(orgId, stripPrllScheme(teamId), stripPrllScheme(userId), {
94
+ role: opts.role,
95
+ });
96
+ printJson({ updated: stripPrllScheme(userId), role: opts.role });
97
+ }
98
+ catch (err) {
99
+ printError(err);
100
+ }
101
+ });
102
+ }
@@ -7,9 +7,10 @@ import { Command } from 'commander';
7
7
  * performs the vendor call in-process.
8
8
  *
9
9
  * Verb surface = the vendor's in-scope interface aggregated: postText
10
- * (send), fetchContactsList + getBriefInfo + getChatroomInfo (contacts),
11
- * getProfile (profile), checkOnline (status). Every verb renders
12
- * agent-readable text by default and `--json` for machine output.
10
+ * (send), platform-retained context (history), fetchContactsList +
11
+ * getBriefInfo + getChatroomInfo (contacts), getProfile (profile),
12
+ * checkOnline (status). History always emits its stable page JSON; the other
13
+ * verbs render agent-readable text by default and accept `--json`.
13
14
  *
14
15
  * INTERNAL research preview: the capability is flag-gated to the internal
15
16
  * org. Design: docs/engineering-design/wechat-channel-design.md §3, §4.
@@ -1 +1 @@
1
- {"version":3,"file":"wechat.d.ts","sourceRoot":"","sources":["../../src/commands/wechat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QAwItD"}
1
+ {"version":3,"file":"wechat.d.ts","sourceRoot":"","sources":["../../src/commands/wechat.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,OAAO,QA0KtD"}
@@ -9,9 +9,10 @@ import { resolveMessageText, TEXT_FILE_OPTION_DESC, TEXT_OPTION_DESC } from '../
9
9
  * performs the vendor call in-process.
10
10
  *
11
11
  * Verb surface = the vendor's in-scope interface aggregated: postText
12
- * (send), fetchContactsList + getBriefInfo + getChatroomInfo (contacts),
13
- * getProfile (profile), checkOnline (status). Every verb renders
14
- * agent-readable text by default and `--json` for machine output.
12
+ * (send), platform-retained context (history), fetchContactsList +
13
+ * getBriefInfo + getChatroomInfo (contacts), getProfile (profile),
14
+ * checkOnline (status). History always emits its stable page JSON; the other
15
+ * verbs render agent-readable text by default and accept `--json`.
15
16
  *
16
17
  * INTERNAL research preview: the capability is flag-gated to the internal
17
18
  * org. Design: docs/engineering-design/wechat-channel-design.md §3, §4.
@@ -52,6 +53,33 @@ export function registerWechatCommands(program) {
52
53
  printError(err);
53
54
  }
54
55
  });
56
+ wechat
57
+ .command('history')
58
+ .description('Read platform-retained context for a WeChat DM or group')
59
+ .requiredOption('--conversation <id>', 'Vendor-native conversation id (a friend wxid, or a room id ending in @chatroom)')
60
+ .option('--before <messageId>', 'Read messages strictly older than this external message id')
61
+ .option('--since <time>', 'Inclusive lower bound: RFC3339 timestamp or YYYY-MM-DD')
62
+ .option('--limit <n>', 'Page size (default 20, max 100)')
63
+ .action(async (opts) => {
64
+ try {
65
+ let limit;
66
+ if (opts.limit !== undefined) {
67
+ limit = Number(opts.limit);
68
+ if (!Number.isInteger(limit) || limit < 1 || limit > 100) {
69
+ throw new Error('--limit must be an integer between 1 and 100');
70
+ }
71
+ }
72
+ const { client, orgId } = resolveCredentials();
73
+ printJson(await client.wechatHistory(orgId, opts.conversation, {
74
+ ...(opts.before ? { before: opts.before } : {}),
75
+ ...(opts.since ? { since: opts.since } : {}),
76
+ ...(limit !== undefined ? { limit } : {}),
77
+ }));
78
+ }
79
+ catch (err) {
80
+ printError(err);
81
+ }
82
+ });
55
83
  wechat
56
84
  .command('contacts')
57
85
  .description('List the account address book with display names: friends, saved groups, followed official accounts')
package/dist/index.js CHANGED
@@ -9,6 +9,8 @@ import { registerMessageCommands } from './commands/messages.js';
9
9
  import { registerTaskCommands } from './commands/tasks.js';
10
10
  import { registerCommentCommands } from './commands/comments.js';
11
11
  import { registerProjectCommands } from './commands/projects.js';
12
+ import { registerProfileCommands } from './commands/profile.js';
13
+ import { registerTeamCommands } from './commands/teams.js';
12
14
  import { registerScheduleCommands } from './commands/schedules.js';
13
15
  import { registerExternalTriggerCommands } from './commands/external-triggers.js';
14
16
  import { registerUserCommands } from './commands/users.js';
@@ -22,6 +24,7 @@ import { registerMachineCommands } from './commands/machines.js';
22
24
  import { registerClipCommands } from './commands/clip.js';
23
25
  import { registerSlackCommands } from './commands/slack.js';
24
26
  import { registerWechatCommands } from './commands/wechat.js';
27
+ import { createUpdateNotifier } from './lib/update-check.js';
25
28
  const require = createRequire(import.meta.url);
26
29
  const pkg = require('../package.json');
27
30
  const program = new Command();
@@ -37,6 +40,8 @@ registerMessageCommands(program);
37
40
  registerTaskCommands(program);
38
41
  registerCommentCommands(program);
39
42
  registerProjectCommands(program);
43
+ registerProfileCommands(program);
44
+ registerTeamCommands(program);
40
45
  registerScheduleCommands(program);
41
46
  registerExternalTriggerCommands(program);
42
47
  registerUserCommands(program);
@@ -50,4 +55,10 @@ registerMachineCommands(program);
50
55
  registerClipCommands(program);
51
56
  registerSlackCommands(program);
52
57
  registerWechatCommands(program);
58
+ const updateNotifier = createUpdateNotifier(pkg.version);
59
+ program.hook('postAction', () => {
60
+ if (process.exitCode === undefined || process.exitCode === 0) {
61
+ updateNotifier.afterSuccessfulCommand();
62
+ }
63
+ });
53
64
  program.parse();
@@ -28,6 +28,13 @@ export type RuntimeContext = {
28
28
  dispatchSourceId?: string;
29
29
  /** Trigger thread root (parel invocation context) — the by-source send's default thread. */
30
30
  threadRootId?: string;
31
+ /**
32
+ * Conversation generation of this execution (parel v2 invocation context,
33
+ * PRLL_GENERATION). Rides every dispatch_source send; the server rejects a
34
+ * mutation whose generation predates a New Session barrier. Absent on v1
35
+ * and lane-bound turns.
36
+ */
37
+ generation?: number;
31
38
  };
32
39
  /**
33
40
  * Read per-dispatch context from ENV + sideband files.
@@ -49,25 +56,36 @@ export declare function resolveRuntimeContext(): RuntimeContext;
49
56
  export declare function resolveSourceDispatchBinding(ctx: RuntimeContext, chatId: string, threadRootId: string | undefined): {
50
57
  source_type: string;
51
58
  source_id: string;
59
+ generation?: number;
52
60
  } | null;
53
61
  /** Typed dispatch binding for a task write, derived from the turn context. */
54
62
  export type TypedDispatchBinding = {
55
- lane: string;
63
+ /** Claimed typed lane (v1 consumers). Absent on the converged transport,
64
+ * where no lane exists and the write binds by dispatch_event_id under the
65
+ * generation fence instead. */
66
+ lane?: string;
56
67
  dispatchEventId: string;
68
+ /** Conversation generation (converged transport, PRLL_GENERATION). */
69
+ generation?: number;
57
70
  /** Set for the first (resolving) update of this dispatch. */
58
71
  effectKey?: string;
59
72
  markCommitted: () => void;
60
73
  };
61
74
  /**
62
- * Resolve the typed-lane binding for `parall task update <taskId>`: the
63
- * current turn must be a typed dispatch (lane target `dsp:<id>`) about this
64
- * exact task. The first update carries the canonical
75
+ * Resolve the typed dispatch binding for `parall task update <taskId>`: the
76
+ * current turn must be a typed dispatch about this exact task.
77
+ *
78
+ * Lane-bound (v1): the first update carries the canonical
65
79
  * `task_update:<dispatch_event_id>` effect key (resolves the WorkItem in the
66
- * same transaction); later updates go keyless (lane-checked only). The
67
- * committed flag lives in a CLI-owned sidecar keyed by the typed lane; when
80
+ * same transaction); later updates go keyless (lane-checked only), with the
81
+ * committed flag in a CLI-owned sidecar keyed by the typed lane. When
68
82
  * PRLL_CONTEXT_DIR is absent the sidecar can't exist, so no effect key is
69
83
  * ever attached (a blind key would make a *different* second update replay
70
84
  * the first one's result instead of applying).
85
+ *
86
+ * Converged (no lane): the caller appends a payload hash to the base key —
87
+ * content addressing gives every distinct update its own idempotent key, so
88
+ * no sidecar state is needed at all.
71
89
  */
72
90
  export declare function resolveTypedDispatchBinding(taskId: string): TypedDispatchBinding | null;
73
91
  /** Per-lane dispatch context (PRLL_CONTEXT_DIR contract), keyed by send target. */
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oGAAoG;IACpG,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAyDtD;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAKnD;AAED,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,EAAE,MAAM,CAAC;IACxB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;GAUG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CAwCvF;AAED,mFAAmF;AACnF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,GACpB,mBAAmB,GAAG,IAAI,CA+E5B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAYzE;AAED,wBAAgB,kBAAkB,IAAI,mBAAmB,CA8BxD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,6DAA6D;AAC7D,MAAM,MAAM,cAAc,GAAG;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,oGAAoG;IACpG,OAAO,EAAE,OAAO,CAAC;IACjB,8EAA8E;IAC9E,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sFAAsF;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,4FAA4F;IAC5F,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAkEtD;AAED;;;;;;;GAOG;AACH,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,cAAc,EACnB,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,MAAM,GAAG,SAAS,GAC/B;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAWxE;AAED,8EAA8E;AAC9E,MAAM,MAAM,oBAAoB,GAAG;IACjC;;oCAEgC;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,eAAe,EAAE,MAAM,CAAC;IACxB,sEAAsE;IACtE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,EAAE,MAAM,IAAI,CAAC;CAC3B,CAAC;AAEF;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI,CA8DvF;AAED,mFAAmF;AACnF,MAAM,MAAM,mBAAmB,GAAG;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,MAAM,CAAC;CACxB,CAAC;AAEF;;;;GAIG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;CAAG;AAE9C;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,YAAY,CAAC,EAAE,MAAM,GACpB,mBAAmB,GAAG,IAAI,CA+E5B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI,CAYzE;AAED,wBAAgB,kBAAkB,IAAI,mBAAmB,CA8BxD"}
@@ -18,10 +18,15 @@ export function resolveRuntimeContext() {
18
18
  const dispatchSourceType = process.env.PRLL_DISPATCH_SOURCE_TYPE?.trim() || undefined;
19
19
  const dispatchSourceId = process.env.PRLL_DISPATCH_SOURCE_ID?.trim() || undefined;
20
20
  const threadRootId = process.env.PRLL_THREAD_ROOT_ID?.trim() || undefined;
21
- let dispatchEventId;
22
- let lane;
23
- let laneTargetUri;
24
- let taskId;
21
+ const generationRaw = process.env.PRLL_GENERATION?.trim();
22
+ const generation = generationRaw && /^\d+$/.test(generationRaw) ? Number(generationRaw) : undefined;
23
+ // Parel v2 surfaces the lane context as plain env (there is no context
24
+ // file inside a parel sandbox); the CC/Codex context file below still wins
25
+ // per-field when both exist.
26
+ let dispatchEventId = process.env.PRLL_DISPATCH_EVENT_ID?.trim() || undefined;
27
+ let lane = process.env.PRLL_DISPATCH_LANE?.trim() || undefined;
28
+ let laneTargetUri = process.env.PRLL_LANE_TARGET_URI?.trim() || undefined;
29
+ let taskId = process.env.PRLL_TASK_ID?.trim() || undefined;
25
30
  if (process.env.PRLL_CONTEXT_FILE) {
26
31
  try {
27
32
  const raw = fs.readFileSync(process.env.PRLL_CONTEXT_FILE, 'utf-8').trim();
@@ -33,10 +38,12 @@ export function resolveRuntimeContext() {
33
38
  stepId ??= ctx.step_id || undefined;
34
39
  if (ctx.no_reply === true)
35
40
  noReply = true;
36
- dispatchEventId = ctx.dispatch_event_id || undefined;
37
- lane = ctx.lane || undefined;
38
- laneTargetUri = ctx.target_uri || undefined;
39
- taskId = ctx.task_id || undefined;
41
+ // The file wins per-field over the parel env fallbacks above, but an
42
+ // absent field must not erase them.
43
+ dispatchEventId = ctx.dispatch_event_id || dispatchEventId;
44
+ lane = ctx.lane || lane;
45
+ laneTargetUri = ctx.target_uri || laneTargetUri;
46
+ taskId = ctx.task_id || taskId;
40
47
  }
41
48
  }
42
49
  catch {
@@ -66,6 +73,7 @@ export function resolveRuntimeContext() {
66
73
  dispatchSourceType,
67
74
  dispatchSourceId,
68
75
  threadRootId,
76
+ generation,
69
77
  };
70
78
  }
71
79
  /**
@@ -83,36 +91,67 @@ export function resolveSourceDispatchBinding(ctx, chatId, threadRootId) {
83
91
  return null;
84
92
  if ((threadRootId ?? '') !== (ctx.threadRootId ?? ''))
85
93
  return null;
86
- return { source_type: ctx.dispatchSourceType, source_id: ctx.dispatchSourceId };
94
+ return {
95
+ source_type: ctx.dispatchSourceType,
96
+ source_id: ctx.dispatchSourceId,
97
+ // v2 executions carry their conversation generation; the server fences
98
+ // stale-generation replies inside the commit transaction.
99
+ ...(ctx.generation !== undefined ? { generation: ctx.generation } : {}),
100
+ };
87
101
  }
88
102
  /**
89
- * Resolve the typed-lane binding for `parall task update <taskId>`: the
90
- * current turn must be a typed dispatch (lane target `dsp:<id>`) about this
91
- * exact task. The first update carries the canonical
103
+ * Resolve the typed dispatch binding for `parall task update <taskId>`: the
104
+ * current turn must be a typed dispatch about this exact task.
105
+ *
106
+ * Lane-bound (v1): the first update carries the canonical
92
107
  * `task_update:<dispatch_event_id>` effect key (resolves the WorkItem in the
93
- * same transaction); later updates go keyless (lane-checked only). The
94
- * committed flag lives in a CLI-owned sidecar keyed by the typed lane; when
108
+ * same transaction); later updates go keyless (lane-checked only), with the
109
+ * committed flag in a CLI-owned sidecar keyed by the typed lane. When
95
110
  * PRLL_CONTEXT_DIR is absent the sidecar can't exist, so no effect key is
96
111
  * ever attached (a blind key would make a *different* second update replay
97
112
  * the first one's result instead of applying).
113
+ *
114
+ * Converged (no lane): the caller appends a payload hash to the base key —
115
+ * content addressing gives every distinct update its own idempotent key, so
116
+ * no sidecar state is needed at all.
98
117
  */
99
118
  export function resolveTypedDispatchBinding(taskId) {
100
119
  const ctx = resolveRuntimeContext();
101
- if (!ctx.lane || !ctx.dispatchEventId)
102
- return null;
103
- if (!ctx.laneTargetUri?.startsWith('dsp:'))
120
+ if (!ctx.dispatchEventId)
104
121
  return null;
105
122
  if (!ctx.taskId || ctx.taskId !== taskId)
106
123
  return null;
107
- const binding = {
108
- lane: ctx.lane,
109
- dispatchEventId: ctx.dispatchEventId,
110
- markCommitted: () => { },
111
- };
124
+ let binding;
125
+ let sidecarKey;
126
+ if (!ctx.lane) {
127
+ // Converged transport: no lane exists — the write binds by
128
+ // dispatch_event_id under the server's generation fence. A context with
129
+ // no generation must NOT produce a binding: an unfenced by-id write
130
+ // would bypass the New Session barrier (the server rejects it anyway —
131
+ // failing closed here keeps the task update itself working as a plain
132
+ // write). Content-addressed keys replace the reply-slot sidecar on this
133
+ // path: the caller appends a payload hash to the base key, so a
134
+ // same-payload retry replays its own key idempotently while a different
135
+ // second update mints a new key and lands as its own Effect.
136
+ if (ctx.generation === undefined)
137
+ return null;
138
+ return {
139
+ dispatchEventId: ctx.dispatchEventId,
140
+ generation: ctx.generation,
141
+ effectKey: `task_update:${ctx.dispatchEventId}`,
142
+ markCommitted: () => { },
143
+ };
144
+ }
145
+ // Lane-bound (v1 consumers): the write rides the claimed typed lane and
146
+ // the reply-slot sidecar arbitrates the single resolving key.
147
+ if (!ctx.laneTargetUri?.startsWith('dsp:'))
148
+ return null;
149
+ binding = { lane: ctx.lane, dispatchEventId: ctx.dispatchEventId, markCommitted: () => { } };
150
+ sidecarKey = ctx.laneTargetUri;
112
151
  const contextDir = process.env.PRLL_CONTEXT_DIR?.trim();
113
152
  if (!contextDir)
114
153
  return binding;
115
- const sidecarPath = laneReplyStateFilePath(contextDir, ctx.laneTargetUri);
154
+ const sidecarPath = laneReplyStateFilePath(contextDir, sidecarKey);
116
155
  let committed = false;
117
156
  try {
118
157
  const sidecar = JSON.parse(fs.readFileSync(sidecarPath, 'utf-8'));
@@ -122,19 +161,22 @@ export function resolveTypedDispatchBinding(taskId) {
122
161
  catch {
123
162
  // No sidecar yet — resolving update not committed.
124
163
  }
125
- if (!committed) {
126
- const dispatchEventId = ctx.dispatchEventId;
127
- binding.effectKey = `task_update:${dispatchEventId}`;
128
- binding.markCommitted = () => {
129
- try {
130
- fs.writeFileSync(sidecarPath, JSON.stringify({ dispatch_event_id: dispatchEventId, reply_committed: true }), 'utf-8');
131
- }
132
- catch {
133
- // Best-effort — a lost sidecar means the next update retries the key
134
- // and the server's effect ledger replies idempotently.
135
- }
136
- };
164
+ if (committed) {
165
+ // Resolving update already landed: later updates go keyless.
166
+ delete binding.effectKey;
167
+ return binding;
137
168
  }
169
+ const dispatchEventId = ctx.dispatchEventId;
170
+ binding.effectKey = `task_update:${dispatchEventId}`;
171
+ binding.markCommitted = () => {
172
+ try {
173
+ fs.writeFileSync(sidecarPath, JSON.stringify({ dispatch_event_id: dispatchEventId, reply_committed: true }), 'utf-8');
174
+ }
175
+ catch {
176
+ // Best-effort — a lost sidecar means the next update retries the key
177
+ // and the server's effect ledger replies idempotently.
178
+ }
179
+ };
138
180
  return binding;
139
181
  }
140
182
  /**
@@ -21,5 +21,5 @@ export declare function ensurePrllScheme(idOrUri: string): string;
21
21
  * than receiving a malformed value.
22
22
  */
23
23
  export declare function parsePositiveInt(raw: string | undefined): number | undefined;
24
- export declare function printError(err: unknown): never;
24
+ export declare function printError(err: unknown, attemptedPayload?: Record<string, unknown>): never;
25
25
  //# sourceMappingURL=output.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAEA,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAE7C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAY,GAAG,IAAI,CAEvE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW5E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,KAAK,CAwC9C"}
1
+ {"version":3,"file":"output.d.ts","sourceRoot":"","sources":["../../src/lib/output.ts"],"names":[],"mappings":"AAEA,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI,CAE7C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAY,GAAG,IAAI,CAEvE;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAExD;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAW5E;AAED,wBAAgB,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,KAAK,CA8C1F"}
@@ -43,7 +43,7 @@ export function parsePositiveInt(raw) {
43
43
  const n = Number(t);
44
44
  return Number.isInteger(n) && n > 0 ? n : undefined;
45
45
  }
46
- export function printError(err) {
46
+ export function printError(err, attemptedPayload) {
47
47
  if (err instanceof ApiError) {
48
48
  // Faithful, parseable line: the server's real message + machine anchors.
49
49
  console.error(JSON.stringify({
@@ -70,7 +70,13 @@ export function printError(err) {
70
70
  ? 'This action can be approved.'
71
71
  : 'This action may be approvable.';
72
72
  const chat = err.resourceUri?.startsWith('prll://cht_') ? err.resourceUri : '<chat_id>';
73
- console.error(`${lead} Request approval:\n parall approvals request --action ${err.action} --resource ${err.resourceUri ?? '<resource>'} --chat ${chat} --title "..." --reason "..."`);
73
+ // When the failing command knows exactly what it tried to write, the
74
+ // proposal command is prefilled with that payload — the deny → propose
75
+ // loop then needs no re-assembly by the caller.
76
+ const payloadArg = attemptedPayload
77
+ ? ` --payload '${JSON.stringify(attemptedPayload).replaceAll("'", `'\\''`)}'`
78
+ : '';
79
+ console.error(`${lead} Request approval:\n parall approvals request --action ${err.action} --resource ${err.resourceUri ?? '<resource>'}${payloadArg} --chat ${chat} --title "..." --reason "..."`);
74
80
  }
75
81
  }
76
82
  }
@@ -0,0 +1,29 @@
1
+ import { spawn } from 'node:child_process';
2
+ export declare const UPDATE_CHECK_TIMEOUT_MS = 1500;
3
+ type UpdateNotifierOptions = {
4
+ now?: () => number;
5
+ cachePath?: string;
6
+ spawnWorker?: (cachePath: string) => void;
7
+ writeHint?: (hint: string) => void;
8
+ disabled?: boolean;
9
+ };
10
+ type RefreshOptions = {
11
+ now?: () => number;
12
+ fetch?: typeof fetch;
13
+ timeoutMs?: number;
14
+ registryUrl?: string;
15
+ };
16
+ export type UpdateNotifier = {
17
+ afterSuccessfulCommand: () => void;
18
+ };
19
+ export declare function isNewerRelease(currentVersion: string, latestVersion: string): boolean;
20
+ /**
21
+ * Acquire the cache's cross-process lock. The token check prevents a stale
22
+ * owner from deleting a replacement lock when it eventually resumes.
23
+ */
24
+ export declare function acquireUpdateStateLock(cachePath: string, now?: () => number): (() => void) | null;
25
+ export declare function spawnUpdateCheckWorker(cachePath: string, spawnImpl?: typeof spawn): void;
26
+ export declare function createUpdateNotifier(currentVersion: string, options?: UpdateNotifierOptions): UpdateNotifier;
27
+ export declare function refreshUpdateCache(cachePath: string, options?: RefreshOptions): Promise<void>;
28
+ export {};
29
+ //# sourceMappingURL=update-check.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check.d.ts","sourceRoot":"","sources":["../../src/lib/update-check.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAW3C,eAAO,MAAM,uBAAuB,OAAQ,CAAC;AAS7C,KAAK,qBAAqB,GAAG;IAC3B,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB,CAAC;AAEF,KAAK,cAAc,GAAG;IACpB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,cAAc,GAAG;IAC3B,sBAAsB,EAAE,MAAM,IAAI,CAAC;CACpC,CAAC;AAQF,wBAAgB,cAAc,CAAC,cAAc,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAUrF;AA4CD;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAAM,MAAiB,GAC3B,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI,CAkErB;AAgBD,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,SAAS,GAAE,OAAO,KAAa,GAAG,IAAI,CAW/F;AAWD,wBAAgB,oBAAoB,CAClC,cAAc,EAAE,MAAM,EACtB,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAyDhB;AAED,wBAAsB,kBAAkB,CACtC,SAAS,EAAE,MAAM,EACjB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC,CA+Bf"}
@@ -0,0 +1,269 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { createHash, randomUUID } from 'node:crypto';
3
+ import * as fs from 'node:fs';
4
+ import * as os from 'node:os';
5
+ import * as path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ const REGISTRY_URL = 'https://registry.npmjs.org/@parall%2Fcli/latest';
8
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
9
+ const RETRY_INTERVAL_MS = 60 * 60 * 1000;
10
+ const LOCK_STALE_MS = 30_000;
11
+ export const UPDATE_CHECK_TIMEOUT_MS = 1_500;
12
+ function releaseParts(version) {
13
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(version);
14
+ if (!match)
15
+ return null;
16
+ return [BigInt(match[1]), BigInt(match[2]), BigInt(match[3])];
17
+ }
18
+ export function isNewerRelease(currentVersion, latestVersion) {
19
+ const current = releaseParts(currentVersion);
20
+ const latest = releaseParts(latestVersion);
21
+ if (!current || !latest)
22
+ return false;
23
+ for (let i = 0; i < current.length; i++) {
24
+ if (latest[i] > current[i])
25
+ return true;
26
+ if (latest[i] < current[i])
27
+ return false;
28
+ }
29
+ return false;
30
+ }
31
+ function defaultCachePath() {
32
+ const envRoot = process.env.XDG_CACHE_HOME?.trim();
33
+ const cacheRoot = envRoot ||
34
+ (process.platform === 'win32' && process.env.LOCALAPPDATA?.trim()) ||
35
+ path.join(os.homedir(), '.cache');
36
+ return path.join(cacheRoot, 'parall', 'update-check.json');
37
+ }
38
+ function readState(cachePath) {
39
+ try {
40
+ const parsed = JSON.parse(fs.readFileSync(cachePath, 'utf8'));
41
+ const state = {};
42
+ if (typeof parsed.latest_version === 'string')
43
+ state.latest_version = parsed.latest_version;
44
+ if (typeof parsed.last_success_at === 'number')
45
+ state.last_success_at = parsed.last_success_at;
46
+ if (typeof parsed.last_attempt_at === 'number')
47
+ state.last_attempt_at = parsed.last_attempt_at;
48
+ if (typeof parsed.last_notified_at === 'number') {
49
+ state.last_notified_at = parsed.last_notified_at;
50
+ }
51
+ return state;
52
+ }
53
+ catch {
54
+ return {};
55
+ }
56
+ }
57
+ function writeState(cachePath, state) {
58
+ const tempPath = `${cachePath}.${process.pid}.tmp`;
59
+ try {
60
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true, mode: 0o700 });
61
+ fs.writeFileSync(tempPath, `${JSON.stringify(state)}\n`, { encoding: 'utf8', mode: 0o600 });
62
+ fs.renameSync(tempPath, cachePath);
63
+ return true;
64
+ }
65
+ catch {
66
+ try {
67
+ fs.unlinkSync(tempPath);
68
+ }
69
+ catch {
70
+ // Best-effort cleanup only.
71
+ }
72
+ return false;
73
+ }
74
+ }
75
+ /**
76
+ * Acquire the cache's cross-process lock. The token check prevents a stale
77
+ * owner from deleting a replacement lock when it eventually resumes.
78
+ */
79
+ export function acquireUpdateStateLock(cachePath, now = Date.now) {
80
+ const lockPath = `${cachePath}.lock`;
81
+ const withTokenGate = (token, action) => {
82
+ const tokenHash = createHash('sha256').update(token).digest('hex');
83
+ const gatePath = `${lockPath}.reclaim-${tokenHash}`;
84
+ const gateToken = `${process.pid}:${randomUUID()}`;
85
+ try {
86
+ fs.writeFileSync(gatePath, gateToken, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ try {
92
+ return action();
93
+ }
94
+ finally {
95
+ try {
96
+ if (fs.readFileSync(gatePath, 'utf8') === gateToken)
97
+ fs.unlinkSync(gatePath);
98
+ }
99
+ catch {
100
+ // A lost/replaced gate is already released from this owner's perspective.
101
+ }
102
+ }
103
+ };
104
+ const tryAcquire = () => {
105
+ const token = `${process.pid}:${randomUUID()}`;
106
+ try {
107
+ fs.mkdirSync(path.dirname(cachePath), { recursive: true, mode: 0o700 });
108
+ fs.writeFileSync(lockPath, token, { encoding: 'utf8', flag: 'wx', mode: 0o600 });
109
+ }
110
+ catch {
111
+ return null;
112
+ }
113
+ return () => {
114
+ withTokenGate(token, () => {
115
+ try {
116
+ if (fs.readFileSync(lockPath, 'utf8') === token)
117
+ fs.unlinkSync(lockPath);
118
+ }
119
+ catch {
120
+ // A lost/replaced lock is already released from this owner's perspective.
121
+ }
122
+ });
123
+ };
124
+ };
125
+ const release = tryAcquire();
126
+ if (release)
127
+ return release;
128
+ let staleToken;
129
+ try {
130
+ staleToken = fs.readFileSync(lockPath, 'utf8');
131
+ if (now() - fs.statSync(lockPath).mtimeMs < LOCK_STALE_MS)
132
+ return null;
133
+ }
134
+ catch (err) {
135
+ return err.code === 'ENOENT' ? tryAcquire() : null;
136
+ }
137
+ return (withTokenGate(staleToken, () => {
138
+ try {
139
+ if (fs.readFileSync(lockPath, 'utf8') !== staleToken)
140
+ return null;
141
+ if (now() - fs.statSync(lockPath).mtimeMs < LOCK_STALE_MS)
142
+ return null;
143
+ fs.unlinkSync(lockPath);
144
+ }
145
+ catch (err) {
146
+ if (err.code !== 'ENOENT')
147
+ return null;
148
+ }
149
+ return tryAcquire();
150
+ }) ?? null);
151
+ }
152
+ function withUpdateStateLock(cachePath, now, update) {
153
+ const release = acquireUpdateStateLock(cachePath, now);
154
+ if (!release)
155
+ return undefined;
156
+ try {
157
+ return update();
158
+ }
159
+ finally {
160
+ release();
161
+ }
162
+ }
163
+ export function spawnUpdateCheckWorker(cachePath, spawnImpl = spawn) {
164
+ const workerPath = fileURLToPath(new URL('../update-check-worker.js', import.meta.url));
165
+ const child = spawnImpl(process.execPath, [workerPath, cachePath], {
166
+ detached: true,
167
+ stdio: 'ignore',
168
+ windowsHide: true,
169
+ });
170
+ child.on('error', () => {
171
+ // A missing/broken Node binary must not turn a completed CLI command into a failure.
172
+ });
173
+ child.unref();
174
+ }
175
+ function updateChecksDisabled() {
176
+ const ci = process.env.CI?.trim().toLowerCase();
177
+ return (process.env.PRLL_DISABLE_UPDATE_CHECK === '1' ||
178
+ process.env.NO_UPDATE_NOTIFIER === '1' ||
179
+ Boolean(ci && ci !== '0' && ci !== 'false'));
180
+ }
181
+ export function createUpdateNotifier(currentVersion, options = {}) {
182
+ const now = options.now ?? Date.now;
183
+ const cachePath = options.cachePath ?? defaultCachePath();
184
+ const spawnWorker = options.spawnWorker ?? spawnUpdateCheckWorker;
185
+ const writeHint = options.writeHint ?? ((hint) => process.stderr.write(hint));
186
+ const disabled = options.disabled ?? updateChecksDisabled();
187
+ return {
188
+ afterSuccessfulCommand: () => {
189
+ if (disabled || !releaseParts(currentVersion))
190
+ return;
191
+ const timestamp = now();
192
+ const decision = withUpdateStateLock(cachePath, now, () => {
193
+ const state = readState(cachePath);
194
+ let hint;
195
+ let changed = false;
196
+ if (state.latest_version &&
197
+ isNewerRelease(currentVersion, state.latest_version) &&
198
+ timestamp - (state.last_notified_at ?? 0) >= CHECK_INTERVAL_MS) {
199
+ hint =
200
+ `Update available: parall ${currentVersion} → ${state.latest_version}. ` +
201
+ 'Upgrade @parall/cli@latest with your package manager.\n';
202
+ state.last_notified_at = timestamp;
203
+ changed = true;
204
+ }
205
+ const successfulCheckIsFresh = timestamp - (state.last_success_at ?? 0) < CHECK_INTERVAL_MS;
206
+ const recentAttempt = timestamp - (state.last_attempt_at ?? 0) < RETRY_INTERVAL_MS;
207
+ const shouldSpawnWorker = !successfulCheckIsFresh && !recentAttempt;
208
+ if (shouldSpawnWorker) {
209
+ state.last_attempt_at = timestamp;
210
+ changed = true;
211
+ }
212
+ if (changed && !writeState(cachePath, state))
213
+ return undefined;
214
+ return { hint, shouldSpawnWorker };
215
+ });
216
+ if (!decision)
217
+ return;
218
+ if (decision.hint) {
219
+ try {
220
+ writeHint(decision.hint);
221
+ }
222
+ catch {
223
+ // A closed output stream must not change the command's exit status.
224
+ }
225
+ }
226
+ if (!decision.shouldSpawnWorker)
227
+ return;
228
+ try {
229
+ spawnWorker(cachePath);
230
+ }
231
+ catch {
232
+ // Update checks never affect the command that triggered them.
233
+ }
234
+ },
235
+ };
236
+ }
237
+ export async function refreshUpdateCache(cachePath, options = {}) {
238
+ const fetchImpl = options.fetch ?? fetch;
239
+ const timeoutMs = options.timeoutMs ?? UPDATE_CHECK_TIMEOUT_MS;
240
+ const controller = new AbortController();
241
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
242
+ try {
243
+ const response = await fetchImpl(options.registryUrl ?? REGISTRY_URL, {
244
+ headers: { accept: 'application/json' },
245
+ signal: controller.signal,
246
+ });
247
+ if (!response.ok)
248
+ return;
249
+ const body = (await response.json());
250
+ if (typeof body.version !== 'string' || !releaseParts(body.version))
251
+ return;
252
+ const latestVersion = body.version;
253
+ const now = options.now ?? Date.now;
254
+ withUpdateStateLock(cachePath, now, () => {
255
+ const state = readState(cachePath);
256
+ writeState(cachePath, {
257
+ ...state,
258
+ latest_version: latestVersion,
259
+ last_success_at: now(),
260
+ });
261
+ });
262
+ }
263
+ catch {
264
+ // Network, timeout, registry, and cache failures are intentionally silent.
265
+ }
266
+ finally {
267
+ clearTimeout(timer);
268
+ }
269
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=update-check-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-check-worker.d.ts","sourceRoot":"","sources":["../src/update-check-worker.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { refreshUpdateCache } from './lib/update-check.js';
3
+ const cachePath = process.argv[2];
4
+ if (cachePath)
5
+ await refreshUpdateCache(cachePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/cli",
3
- "version": "1.55.3",
3
+ "version": "1.55.4",
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.55.3",
40
- "@parall/sdk": "1.55.3"
39
+ "@parall/agent-core": "1.55.4",
40
+ "@parall/sdk": "1.55.4"
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.55.3"
46
+ "@parall/agent-core": "1.55.4"
47
47
  },
48
48
  "scripts": {
49
49
  "build": "tsc",