@parall/cli 1.35.0 → 1.36.1

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,70 @@
1
+ import { resolveCredentials } from '../lib/client.js';
2
+ import { parsePositiveInt, printError, printJson, stripPrllScheme } from '../lib/output.js';
3
+ // Short aliases agents pass via --types map to the server's entity names.
4
+ const TYPE_ALIASES = {
5
+ m: 'message',
6
+ message: 'message',
7
+ messages: 'message',
8
+ t: 'task',
9
+ task: 'task',
10
+ tasks: 'task',
11
+ w: 'wiki',
12
+ wiki: 'wiki',
13
+ };
14
+ /**
15
+ * Normalize a `--types m,t,w` flag into the server's CSV of canonical entity
16
+ * names (`message,task,wiki`). Unknown tokens pass through unchanged so the
17
+ * server can reject/ignore them. Returns undefined for an empty/absent flag so
18
+ * the server applies its default (search all three).
19
+ */
20
+ export function normalizeSearchTypes(raw) {
21
+ if (!raw)
22
+ return undefined;
23
+ const mapped = raw
24
+ .split(',')
25
+ .map((s) => s.trim().toLowerCase())
26
+ .filter(Boolean)
27
+ .map((s) => TYPE_ALIASES[s] ?? s);
28
+ const unique = [...new Set(mapped)];
29
+ return unique.length > 0 ? unique.join(',') : undefined;
30
+ }
31
+ /** Build the SDK search params from the positional query + CLI flags. */
32
+ export function buildSearchParams(query, opts) {
33
+ const params = { q: query };
34
+ const types = normalizeSearchTypes(opts.types);
35
+ if (types)
36
+ params.types = types;
37
+ if (opts.channel !== undefined)
38
+ params.chat_id = stripPrllScheme(opts.channel);
39
+ // Only forward a clean positive integer; a bad --limit falls through to the
40
+ // server default rather than sending NaN/1.5/-2.
41
+ const limit = parsePositiveInt(opts.limit);
42
+ if (limit !== undefined)
43
+ params.limit = limit;
44
+ if (opts.wikiType !== undefined)
45
+ params.wiki_type = opts.wikiType;
46
+ if (opts.since !== undefined)
47
+ params.since = opts.since;
48
+ return params;
49
+ }
50
+ export function registerSearchCommands(program) {
51
+ program
52
+ .command('search')
53
+ .description('Unified semantic search across messages, tasks, and wiki (org-scoped)')
54
+ .argument('<query>', 'Search query')
55
+ .option('--types <list>', 'Comma-separated entity types: m[essage], t[ask], w[iki] (default: all)')
56
+ .option('--channel <chatId>', 'Narrow MESSAGE results to one chat — tasks/wiki unaffected (prll://cht_… or cht_…)')
57
+ .option('--limit <n>', 'Max results per entity type (server caps at 50)')
58
+ .option('--wiki-type <type>', 'Narrow wiki results to a frontmatter document type')
59
+ .option('--since <date>', 'Only messages/tasks at or after this time (RFC3339 or YYYY-MM-DD); not applied to wiki')
60
+ .action(async (query, opts) => {
61
+ try {
62
+ const { client, orgId } = resolveCredentials();
63
+ const result = await client.search(orgId, buildSearchParams(query, opts));
64
+ printJson(result);
65
+ }
66
+ catch (err) {
67
+ printError(err);
68
+ }
69
+ });
70
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAoSpD"}
1
+ {"version":3,"file":"tasks.d.ts","sourceRoot":"","sources":["../../src/commands/tasks.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAQpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAqTpD"}
@@ -148,6 +148,21 @@ export function registerTaskCommands(program) {
148
148
  printError(err);
149
149
  }
150
150
  });
151
+ tasks
152
+ .command('assigned')
153
+ .description("List pending tasks (todo + in_progress) on a member's plate, including subtasks assigned to them")
154
+ .argument('[memberId]', 'Member user ID (defaults to the authenticated user)')
155
+ .action(async (memberId) => {
156
+ try {
157
+ const { client, orgId } = resolveCredentials();
158
+ const targetId = memberId ? stripPrllScheme(memberId) : (await client.getMe()).id;
159
+ const result = await client.getMemberTasksAll(orgId, targetId);
160
+ printJson(result);
161
+ }
162
+ catch (err) {
163
+ printError(err);
164
+ }
165
+ });
151
166
  // ---- Comments subgroup ----
152
167
  const comments = tasks.command('comments').description('Manage task comments');
153
168
  comments
@@ -1 +1 @@
1
- {"version":3,"file":"wiki.d.ts","sourceRoot":"","sources":["../../src/commands/wiki.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAwBpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAy3BpD"}
1
+ {"version":3,"file":"wiki.d.ts","sourceRoot":"","sources":["../../src/commands/wiki.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAyBpC,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,OAAO,QAk/BpD"}
@@ -1,5 +1,7 @@
1
+ import { promises as fs } from 'node:fs';
1
2
  import { resolveCredentials, resolveRuntimeContext } from '../lib/client.js';
2
3
  import { printError, printJson } from '../lib/output.js';
4
+ import { deleteWikiFile, downloadWikiFile, uploadWikiFile } from '../lib/wiki-files.js';
3
5
  import { getAfcsDiff, getAfcsStatus, getWikiBlob, getWikiChangesetDetail, getWikiChangesetDiff, getWikiLog, getWikiOutline, getWikiSection, getWikiTree, listWikiChangesets, queryWiki, requestWikiAccess, resetWikiWorkspace, listWikis, proposeWikiChangeset, searchWiki, syncAllMounts, watchMounts, } from '../lib/wiki.js';
4
6
  export function registerWikiCommands(program) {
5
7
  const wiki = program.command('wiki').description('Parall Wiki — read, edit, and propose changes');
@@ -354,6 +356,94 @@ export function registerWikiCommands(program) {
354
356
  printError(error);
355
357
  }
356
358
  });
359
+ // ---- Binary files (upload / get / delete) ----
360
+ const file = wiki
361
+ .command('file')
362
+ .description('Upload, download, and delete binary wiki files (images, PDFs, archives)');
363
+ file
364
+ .command('upload')
365
+ .description('Upload a binary file. Direct-commits to the default branch (maintain); with --changeset, writes into a proposal branch (read + author). Text files go through changesets, not uploads.')
366
+ .argument('<localPath>', 'Local file to upload')
367
+ .argument('<repoPath>', 'Destination path inside the wiki (e.g. assets/diagram.png)')
368
+ .argument('[wiki]', 'Wiki ID or slug (auto-resolves if org has one wiki)')
369
+ .option('--changeset <changesetId>', "Upload into this changeset's feature branch instead of the default branch")
370
+ .option('--message <message>', 'Commit message (default: "Upload <path>")')
371
+ .action(async (localPath, repoPath, wikiRef, options) => {
372
+ try {
373
+ const ctx = resolveCredentials();
374
+ const content = await fs.readFile(localPath);
375
+ const result = await uploadWikiFile(ctx, wikiRef, {
376
+ content,
377
+ toPath: repoPath,
378
+ changesetId: options.changeset,
379
+ message: options.message,
380
+ });
381
+ const where = result.target === 'changeset' ? `changeset ${result.changeset_id}` : 'default branch';
382
+ process.stderr.write(`Uploaded ${result.path} (${result.size} bytes, ${result.stored_as}) → ${where}\n`);
383
+ printJson(result);
384
+ }
385
+ catch (error) {
386
+ printError(error);
387
+ }
388
+ });
389
+ file
390
+ .command('get')
391
+ .description('Download a wiki file as raw bytes (LFS pointers are smudged to real bytes). Writes to stdout, or to --output. Use --ref to read a specific commit / changeset branch.')
392
+ .argument('<repoPath>', 'File path inside the wiki')
393
+ .argument('[wiki]', 'Wiki ID or slug (auto-resolves if org has one wiki)')
394
+ .option('--ref <ref>', 'Branch, commit SHA, or changeset branch ref (default: wiki default branch)')
395
+ .option('-o, --output <localPath>', 'Write bytes to this local file instead of stdout')
396
+ .action(async (repoPath, wikiRef, options) => {
397
+ try {
398
+ const ctx = resolveCredentials();
399
+ const result = await downloadWikiFile(ctx, wikiRef, {
400
+ path: repoPath,
401
+ ref: options.ref,
402
+ });
403
+ if (options.output) {
404
+ await fs.writeFile(options.output, result.content);
405
+ process.stderr.write(`Wrote ${result.size} bytes → ${options.output}\n`);
406
+ // Buffer is omitted from JSON output; report metadata + where it went.
407
+ printJson({
408
+ wiki_id: result.wiki_id,
409
+ wiki_slug: result.wiki_slug,
410
+ path: result.path,
411
+ ref: result.ref,
412
+ size: result.size,
413
+ output: options.output,
414
+ });
415
+ }
416
+ else {
417
+ // Raw bytes to stdout (binary-safe); metadata to stderr. No JSON on
418
+ // stdout here — it would corrupt the byte stream.
419
+ process.stderr.write(`${result.path} (${result.size} bytes${result.ref ? `, ref: ${result.ref}` : ''})\n`);
420
+ process.stdout.write(result.content);
421
+ }
422
+ }
423
+ catch (error) {
424
+ printError(error);
425
+ }
426
+ });
427
+ file
428
+ .command('delete')
429
+ .description('Delete a binary file from the wiki default branch (maintain). Text deletes must go through a changeset. The blob stays reachable in git history.')
430
+ .argument('<repoPath>', 'File path inside the wiki')
431
+ .argument('[wiki]', 'Wiki ID or slug (auto-resolves if org has one wiki)')
432
+ .option('--message <message>', 'Commit message (default: "Delete <path>")')
433
+ .action(async (repoPath, wikiRef, options) => {
434
+ try {
435
+ const ctx = resolveCredentials();
436
+ const result = await deleteWikiFile(ctx, wikiRef, {
437
+ path: repoPath,
438
+ message: options.message,
439
+ });
440
+ process.stderr.write(`Deleted ${result.path}\n`);
441
+ printJson(result);
442
+ }
443
+ catch (error) {
444
+ printError(error);
445
+ }
446
+ });
357
447
  // ---- Content browsing (search, query, outline, section) ----
358
448
  wiki
359
449
  .command('search')
package/dist/index.js CHANGED
@@ -10,11 +10,13 @@ import { registerTaskCommands } from './commands/tasks.js';
10
10
  import { registerCommentCommands } from './commands/comments.js';
11
11
  import { registerProjectCommands } from './commands/projects.js';
12
12
  import { registerScheduleCommands } from './commands/schedules.js';
13
+ import { registerExternalTriggerCommands } from './commands/external-triggers.js';
13
14
  import { registerUserCommands } from './commands/users.js';
14
15
  import { registerWikiCommands } from './commands/wiki.js';
15
16
  import { registerMcpCommands } from './commands/mcp.js';
16
17
  import { registerNoReplyCommands } from './commands/no-reply.js';
17
18
  import { registerRefCommands } from './commands/refs.js';
19
+ import { registerSearchCommands } from './commands/search.js';
18
20
  import { registerFileCommands } from './commands/files.js';
19
21
  import { registerMachineCommands } from './commands/machines.js';
20
22
  import { registerClipCommands } from './commands/clip.js';
@@ -34,11 +36,13 @@ registerTaskCommands(program);
34
36
  registerCommentCommands(program);
35
37
  registerProjectCommands(program);
36
38
  registerScheduleCommands(program);
39
+ registerExternalTriggerCommands(program);
37
40
  registerUserCommands(program);
38
41
  registerWikiCommands(program);
39
42
  registerMcpCommands(program);
40
43
  registerNoReplyCommands(program);
41
44
  registerRefCommands(program);
45
+ registerSearchCommands(program);
42
46
  registerFileCommands(program);
43
47
  registerMachineCommands(program);
44
48
  registerClipCommands(program);
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AACA,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;CAClB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAiCtD;AAED,wBAAgB,kBAAkB,IAAI,mBAAmB,CAwBxD"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/lib/client.ts"],"names":[],"mappings":"AACA,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;CAClB,CAAC;AAEF;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,IAAI,cAAc,CAiCtD;AAED,wBAAgB,kBAAkB,IAAI,mBAAmB,CA8BxD"}
@@ -59,6 +59,12 @@ export function resolveCredentials() {
59
59
  return {
60
60
  client: new ParallClient({
61
61
  baseUrl: url,
62
+ // Wiki endpoints (/wiki/v1) are served by wiki-service. Staging/prod front
63
+ // both behind one gateway (PRLL_WIKI_URL unset → falls back to the api URL,
64
+ // unchanged); local dev runs them on separate ports, so PRLL_WIKI_URL routes
65
+ // wiki calls (changeset, upload, file get, …) to :8090 while chat/task/
66
+ // comment/ref calls stay on PRLL_API_URL.
67
+ wikiBaseUrl: process.env.PRLL_WIKI_URL?.trim() || url,
62
68
  token: apiKey,
63
69
  swimlaneName: process.env.PRLL_SWIMLANE_NAME,
64
70
  }),
@@ -9,5 +9,17 @@ export declare function printRefHint(entityId: string, action?: string): void;
9
9
  * Allows CLI commands to accept both raw IDs (`tsk_abc`) and URIs (`prll://tsk_abc`).
10
10
  */
11
11
  export declare function stripPrllScheme(idOrUri: string): string;
12
+ /**
13
+ * Ensure a value carries the `prll://` scheme — the inverse of stripPrllScheme.
14
+ * Lets URI-consuming commands accept both `tsk_abc` and `prll://tsk_abc`.
15
+ */
16
+ export declare function ensurePrllScheme(idOrUri: string): string;
17
+ /**
18
+ * Parse a CLI numeric flag (string) into a positive integer, or undefined when
19
+ * absent or not a clean positive integer (NaN / fractional / <= 0). Callers omit
20
+ * the param on undefined so the server applies its own default + bounds rather
21
+ * than receiving a malformed value.
22
+ */
23
+ export declare function parsePositiveInt(raw: string | undefined): number | undefined;
12
24
  export declare function printError(err: unknown): never;
13
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,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,GAAG,KAAK,CAwC9C"}
@@ -16,6 +16,33 @@ export function printRefHint(entityId, action = 'Created') {
16
16
  export function stripPrllScheme(idOrUri) {
17
17
  return idOrUri.startsWith('prll://') ? idOrUri.slice(7) : idOrUri;
18
18
  }
19
+ /**
20
+ * Ensure a value carries the `prll://` scheme — the inverse of stripPrllScheme.
21
+ * Lets URI-consuming commands accept both `tsk_abc` and `prll://tsk_abc`.
22
+ */
23
+ export function ensurePrllScheme(idOrUri) {
24
+ return idOrUri.startsWith('prll://') ? idOrUri : `prll://${idOrUri}`;
25
+ }
26
+ /**
27
+ * Parse a CLI numeric flag (string) into a positive integer, or undefined when
28
+ * absent or not a clean positive integer (NaN / fractional / <= 0). Callers omit
29
+ * the param on undefined so the server applies its own default + bounds rather
30
+ * than receiving a malformed value.
31
+ */
32
+ export function parsePositiveInt(raw) {
33
+ if (raw === undefined)
34
+ return undefined;
35
+ // Plain decimal digits only — reject the scientific/hex/float forms Number()
36
+ // would otherwise coerce (1e2, 0x10, 1.5) so "clean positive integer" holds.
37
+ const t = raw.trim();
38
+ if (!/^\d+$/.test(t))
39
+ return undefined;
40
+ // The regex alone is NOT enough: a digit-only string of hundreds of digits
41
+ // overflows Number() to Infinity, which Number.isInteger rejects — so this
42
+ // guard still does real work (keeps `limit=Infinity` off the wire).
43
+ const n = Number(t);
44
+ return Number.isInteger(n) && n > 0 ? n : undefined;
45
+ }
19
46
  export function printError(err) {
20
47
  if (err instanceof ApiError) {
21
48
  // Faithful, parseable line: the server's real message + machine anchors.
@@ -0,0 +1,76 @@
1
+ import type { WikiStoredAs } from '@parall/sdk';
2
+ import { type ParallContext } from './wiki.js';
3
+ /** Hard upload ceiling — mirrors server `handler.MaxWikiUploadBytes` (100 MiB). */
4
+ export declare const MAX_WIKI_UPLOAD_BYTES: number;
5
+ export type WikiFileUploadResult = {
6
+ wiki_id: string;
7
+ wiki_slug: string;
8
+ wiki_name: string;
9
+ path: string;
10
+ size: number;
11
+ stored_as: WikiStoredAs;
12
+ commit_sha: string;
13
+ content_sha: string;
14
+ /** Where the write landed: the default branch (direct) or a changeset branch. */
15
+ target: 'default_branch' | 'changeset';
16
+ /** Present only when uploaded into a changeset's feature branch. */
17
+ changeset_id?: string;
18
+ };
19
+ export type WikiFileDownloadResult = {
20
+ wiki_id: string;
21
+ wiki_slug: string;
22
+ wiki_name: string;
23
+ path: string;
24
+ /** The ref that was read (omitted when defaulting to the wiki's default branch). */
25
+ ref?: string;
26
+ size: number;
27
+ /** Raw file bytes (LFS pointers are smudged server-side — these are the real bytes). */
28
+ content: Buffer;
29
+ };
30
+ export type WikiFileDeleteResult = {
31
+ wiki_id: string;
32
+ wiki_slug: string;
33
+ wiki_name: string;
34
+ path: string;
35
+ };
36
+ export type UploadWikiFileOptions = {
37
+ /** Destination path inside the wiki repo, e.g. "assets/diagram.png". */
38
+ toPath: string;
39
+ /** Raw file bytes to upload. */
40
+ content: Buffer;
41
+ /** When set, write into this changeset's feature branch (read + author) instead
42
+ * of direct-committing to the default branch (maintain). */
43
+ changesetId?: string;
44
+ /** Optional commit message (server defaults to "Upload {path}"). */
45
+ message?: string;
46
+ };
47
+ /**
48
+ * Upload a binary file to a wiki. With `changesetId`, writes into that
49
+ * changeset's feature branch (the reader "propose with an image" path);
50
+ * without it, direct-commits to the default branch (maintain). Text content is
51
+ * rejected locally with a pointer to the changeset flow so the agent doesn't
52
+ * round-trip into a 422.
53
+ */
54
+ export declare function uploadWikiFile(ctx: ParallContext, wikiRef: string | undefined, options: UploadWikiFileOptions): Promise<WikiFileUploadResult>;
55
+ /**
56
+ * Download a wiki file's raw bytes via the bearer-authenticated
57
+ * `GET /files/{path}?ref=` endpoint. The server smudges LFS pointers, so the
58
+ * agent gets the real binary — never the few-line pointer that lands on disk
59
+ * from a plain `wiki sync` (the runtime image has no git-lfs). `ref` reads any
60
+ * branch / commit (including a changeset feature branch); it defaults to the
61
+ * wiki's default branch.
62
+ */
63
+ export declare function downloadWikiFile(ctx: ParallContext, wikiRef: string | undefined, options: {
64
+ path: string;
65
+ ref?: string;
66
+ }): Promise<WikiFileDownloadResult>;
67
+ /**
68
+ * Delete a binary file from the wiki's default branch (maintain). Text deletes
69
+ * must go through a changeset; the server returns 422 USE_CHANGESET for those.
70
+ * The blob stays reachable via git history — this only removes it from HEAD.
71
+ */
72
+ export declare function deleteWikiFile(ctx: ParallContext, wikiRef: string | undefined, options: {
73
+ path: string;
74
+ message?: string;
75
+ }): Promise<WikiFileDeleteResult>;
76
+ //# sourceMappingURL=wiki-files.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wiki-files.d.ts","sourceRoot":"","sources":["../../src/lib/wiki-files.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAGL,KAAK,aAAa,EAKnB,MAAM,WAAW,CAAC;AAoBnB,mFAAmF;AACnF,eAAO,MAAM,qBAAqB,QAAoB,CAAC;AAEvD,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,YAAY,CAAC;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,iFAAiF;IACjF,MAAM,EAAE,gBAAgB,GAAG,WAAW,CAAC;IACvC,oEAAoE;IACpE,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,wFAAwF;IACxF,OAAO,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,gCAAgC;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB;gEAC4D;IAC5D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oEAAoE;IACpE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;;;;;GAMG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CA4D/B;AAED;;;;;;;GAOG;AACH,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,GACtC,OAAO,CAAC,sBAAsB,CAAC,CAsCjC;AAED;;;;GAIG;AACH,wBAAsB,cAAc,CAClC,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,MAAM,GAAG,SAAS,EAC3B,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAC1C,OAAO,CAAC,oBAAoB,CAAC,CAa/B"}
@@ -0,0 +1,169 @@
1
+ import { isWikiTextContent, normalizeRequiredWikiPath, resolveApiToken, resolveWikiRef, resolveWikiServiceBaseUrl, TEXT_MAX_BYTES, } from './wiki.js';
2
+ // ---------------------------------------------------------------------------
3
+ // Wiki binary file CRUD (Phase 4 of wiki-file-storage-design).
4
+ //
5
+ // Text files keep flowing through the changeset workspace (sync → edit →
6
+ // propose); this module is the agent-facing surface for *binary* assets —
7
+ // images, PDFs, archives — that can't be diffed and must not be base64-inlined
8
+ // into changeset JSON. It wraps the wiki-service endpoints:
9
+ // - POST /uploads (maintain, direct-commit default branch)
10
+ // - POST /changesets/{csId}/files (read + author, into a feature branch)
11
+ // - GET /files/{path}?ref= (read, raw bytes — bearer auth, never a
12
+ // browser <img> which can't send a token)
13
+ // - DELETE /files?path= (maintain)
14
+ //
15
+ // Routing/size/text classification is the server's authority; the local
16
+ // pre-checks here just turn the common mistakes into instant, actionable CLI
17
+ // errors instead of a wasted upload that bounces off a 422/413.
18
+ // ---------------------------------------------------------------------------
19
+ /** Hard upload ceiling — mirrors server `handler.MaxWikiUploadBytes` (100 MiB). */
20
+ export const MAX_WIKI_UPLOAD_BYTES = 100 * 1024 * 1024;
21
+ /**
22
+ * Upload a binary file to a wiki. With `changesetId`, writes into that
23
+ * changeset's feature branch (the reader "propose with an image" path);
24
+ * without it, direct-commits to the default branch (maintain). Text content is
25
+ * rejected locally with a pointer to the changeset flow so the agent doesn't
26
+ * round-trip into a 422.
27
+ */
28
+ export async function uploadWikiFile(ctx, wikiRef, options) {
29
+ const wiki = await resolveWikiRef(ctx, wikiRef);
30
+ const toPath = normalizeRequiredWikiPath(options.toPath);
31
+ const content = options.content;
32
+ const changesetId = options.changesetId?.trim() || undefined;
33
+ if (content.length === 0) {
34
+ throw new Error(`cannot upload ${toPath}: file is empty`);
35
+ }
36
+ if (content.length > MAX_WIKI_UPLOAD_BYTES) {
37
+ throw new Error(`cannot upload ${toPath}: file is ${content.length} bytes, ` +
38
+ `over the ${MAX_WIKI_UPLOAD_BYTES}-byte (100 MiB) upload limit`);
39
+ }
40
+ // LFS pointer text is a server-generated artifact; uploading it directly
41
+ // would commit a dangling pointer. Reject before the text check (a pointer
42
+ // is valid UTF-8 and would otherwise be misrouted to "use a changeset").
43
+ if (looksLikeLfsPointer(content)) {
44
+ throw new Error(`cannot upload ${toPath}: this is a Git-LFS pointer, not real bytes. ` +
45
+ 'Upload the underlying binary and the server generates the pointer.');
46
+ }
47
+ // Mirror the server's text gate (filetype: len <= TextMaxBytes && IsText):
48
+ // text goes through changesets so the diff stays reviewable.
49
+ if (content.length <= TEXT_MAX_BYTES && isWikiTextContent(content)) {
50
+ throw new Error(`cannot upload ${toPath}: it looks like a text file, and text changes go ` +
51
+ 'through changesets (sync → edit → `parall wiki changeset create`), not uploads. ' +
52
+ 'Uploads are for binary assets (images, PDFs, archives).');
53
+ }
54
+ // Node Buffer is a Uint8Array (a valid BlobPart); Blob copies the bytes.
55
+ const blob = new Blob([content]);
56
+ const resp = changesetId
57
+ ? await ctx.client.uploadWikiFileToChangeset(ctx.orgId, wiki.id, changesetId, {
58
+ path: toPath,
59
+ file: blob,
60
+ message: options.message,
61
+ })
62
+ : await ctx.client.uploadWikiFile(ctx.orgId, wiki.id, {
63
+ path: toPath,
64
+ file: blob,
65
+ message: options.message,
66
+ });
67
+ return {
68
+ wiki_id: wiki.id,
69
+ wiki_slug: wiki.slug,
70
+ wiki_name: wiki.name,
71
+ path: resp.path,
72
+ size: resp.size,
73
+ stored_as: resp.stored_as,
74
+ commit_sha: resp.commit_sha,
75
+ content_sha: resp.content_sha,
76
+ target: changesetId ? 'changeset' : 'default_branch',
77
+ ...(changesetId ? { changeset_id: changesetId } : {}),
78
+ };
79
+ }
80
+ /**
81
+ * Download a wiki file's raw bytes via the bearer-authenticated
82
+ * `GET /files/{path}?ref=` endpoint. The server smudges LFS pointers, so the
83
+ * agent gets the real binary — never the few-line pointer that lands on disk
84
+ * from a plain `wiki sync` (the runtime image has no git-lfs). `ref` reads any
85
+ * branch / commit (including a changeset feature branch); it defaults to the
86
+ * wiki's default branch.
87
+ */
88
+ export async function downloadWikiFile(ctx, wikiRef, options) {
89
+ const wiki = await resolveWikiRef(ctx, wikiRef);
90
+ const filePath = normalizeRequiredWikiPath(options.path);
91
+ const ref = options.ref?.trim() || undefined;
92
+ const token = resolveApiToken(ctx);
93
+ const base = wikiServiceBaseUrl(ctx);
94
+ // Encode each path segment so spaces / unicode in the path don't break the
95
+ // URL; the server's chi `/files/*` wildcard decodes them back.
96
+ const encodedPath = filePath.split('/').map(encodeURIComponent).join('/');
97
+ let url = `${base}/wiki/v1/orgs/${ctx.orgId}/wikis/${wiki.id}/files/${encodedPath}`;
98
+ if (ref) {
99
+ url += `?ref=${encodeURIComponent(ref)}`;
100
+ }
101
+ const headers = { Authorization: `Bearer ${token}` };
102
+ if (process.env.PRLL_SWIMLANE_NAME) {
103
+ headers['X-Prll-Swimlane'] = process.env.PRLL_SWIMLANE_NAME;
104
+ }
105
+ const response = await fetch(url, { headers });
106
+ if (!response.ok) {
107
+ const text = await response.text().catch(() => '');
108
+ throw new Error(`wiki-service GET ${url}: ${response.status} ${response.statusText}${text ? ` — ${text}` : ''}`);
109
+ }
110
+ const content = Buffer.from(await response.arrayBuffer());
111
+ return {
112
+ wiki_id: wiki.id,
113
+ wiki_slug: wiki.slug,
114
+ wiki_name: wiki.name,
115
+ path: filePath,
116
+ ref,
117
+ size: content.length,
118
+ content,
119
+ };
120
+ }
121
+ /**
122
+ * Delete a binary file from the wiki's default branch (maintain). Text deletes
123
+ * must go through a changeset; the server returns 422 USE_CHANGESET for those.
124
+ * The blob stays reachable via git history — this only removes it from HEAD.
125
+ */
126
+ export async function deleteWikiFile(ctx, wikiRef, options) {
127
+ const wiki = await resolveWikiRef(ctx, wikiRef);
128
+ const filePath = normalizeRequiredWikiPath(options.path);
129
+ await ctx.client.deleteWikiFile(ctx.orgId, wiki.id, {
130
+ path: filePath,
131
+ message: options.message?.trim() || undefined,
132
+ });
133
+ return {
134
+ wiki_id: wiki.id,
135
+ wiki_slug: wiki.slug,
136
+ wiki_name: wiki.name,
137
+ path: filePath,
138
+ };
139
+ }
140
+ // ---------------------------------------------------------------------------
141
+ // Internal helpers
142
+ // ---------------------------------------------------------------------------
143
+ /**
144
+ * Wiki-service base URL for the raw-byte download, reusing the shared resolver
145
+ * (SSOT in wiki.ts, same chain as sync/bulk-download) and turning a missing
146
+ * base into an actionable error instead of a malformed-URL fetch failure.
147
+ */
148
+ function wikiServiceBaseUrl(ctx) {
149
+ const base = resolveWikiServiceBaseUrl(ctx);
150
+ if (!base) {
151
+ throw new Error('wiki-service base URL is required (set PRLL_WIKI_URL or PRLL_API_URL) to download wiki files');
152
+ }
153
+ return base;
154
+ }
155
+ /**
156
+ * Pre-check mirror of the server's `gitea.IsLFSPointer` first-line gate. A
157
+ * full parse isn't needed — matching the version line is enough to give the
158
+ * LFS-specific message and avoid the changeset↔upload loop the design warns
159
+ * about. Anything pointer-shaped but malformed falls through to the server's
160
+ * authoritative classifier.
161
+ */
162
+ function looksLikeLfsPointer(content) {
163
+ if (content.length === 0 || content.length > 1024) {
164
+ return false;
165
+ }
166
+ const nl = content.indexOf(0x0a);
167
+ const firstLine = content.subarray(0, nl >= 0 ? nl : content.length).toString('utf8');
168
+ return firstLine.startsWith('version https://git-lfs.github.com/spec/v1');
169
+ }
@@ -26,8 +26,9 @@ export type WikiToolDefinition = {
26
26
  /**
27
27
  * Build the full wiki native tool registry bound to a Parall context (credentials,
28
28
  * agent env, local mount). Returns one definition per wiki capability (list, tree,
29
- * status, diff, blob, search, query, outline, section, changeset diff, propose),
30
- * each finalized with input validation. Shared by the MCP server and the eval harness.
29
+ * status, diff, blob, search, query, outline, section, changeset diff, propose,
30
+ * binary file upload/get/delete), each finalized with input validation. Shared by
31
+ * the MCP server and the eval harness.
31
32
  */
32
33
  export declare function buildWikiTools(ctx: ParallContext): WikiToolDefinition[];
33
34
  //# sourceMappingURL=wiki-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wiki-tools.d.ts","sourceRoot":"","sources":["../../src/lib/wiki-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAE5B,OAAO,EASL,KAAK,aAAa,EAInB,MAAM,WAAW,CAAC;AAEnB;;;;;;;;;GASG;AAEH,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,YAAY,EAAE,CAAC,CAAC,WAAW,CAAC;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC9E,CAAC;AAoBF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,kBAAkB,EAAE,CAiiBvE"}
1
+ {"version":3,"file":"wiki-tools.d.ts","sourceRoot":"","sources":["../../src/lib/wiki-tools.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,CAAC,MAAM,QAAQ,CAAC;AAG5B,OAAO,EASL,KAAK,aAAa,EAInB,MAAM,WAAW,CAAC;AAUnB;;;;;;;;;GASG;AAEH,MAAM,MAAM,uBAAuB,GAAG;IACpC,iBAAiB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,QAAQ,EAAE,OAAO,CAAC;IAClB,WAAW,EAAE,CAAC,CAAC,WAAW,CAAC;IAC3B,YAAY,EAAE,CAAC,CAAC,WAAW,CAAC;IAC5B,OAAO,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,uBAAuB,CAAC,CAAC;CAC9E,CAAC;AAmCF;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,kBAAkB,EAAE,CAwrBvE"}