@parall/cli 1.34.0 → 1.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"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,QAq2BpD"}
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"}
@@ -33,10 +33,10 @@ export function registerWikiCommands(program) {
33
33
  .command('sync')
34
34
  .description('Sync wiki files to local workspace. First run downloads all files; subsequent runs pull updates.')
35
35
  .argument('[wiki]', 'Wiki ID or slug (omit to sync all wikis)')
36
- .action(async (_wiki) => {
36
+ .action(async (wikiRef) => {
37
37
  try {
38
38
  const ctx = resolveCredentials();
39
- const result = await syncAllMounts(ctx);
39
+ const result = await syncAllMounts(ctx, wikiRef);
40
40
  for (const entry of result.synced) {
41
41
  process.stderr.write(`wiki ${entry.slug} synced → ${entry.path}\n`);
42
42
  }
@@ -55,22 +55,21 @@ export function registerWikiCommands(program) {
55
55
  });
56
56
  wiki
57
57
  .command('status')
58
- .description('Show local changes, pending changesets, and permission summary.')
58
+ .description('Show local changes and pending changesets.')
59
59
  .argument('[wiki]', 'Wiki ID or slug (auto-resolves if org has one wiki)')
60
60
  .action(async (wikiRef) => {
61
61
  try {
62
62
  const ctx = resolveCredentials();
63
63
  const result = await getAfcsStatus(ctx, wikiRef);
64
64
  process.stderr.write(`Wiki: ${result.wiki_name} (${result.wiki_slug})\n`);
65
- process.stderr.write(`Mount: ${result.mount_path}\n`);
66
- process.stderr.write(`Mode: ${result.mode}\n\n`);
65
+ process.stderr.write(`Mount: ${result.mount_path}\n\n`);
67
66
  if (result.local_changes.length === 0) {
68
67
  process.stderr.write('Local changes: none\n');
69
68
  }
70
69
  else {
71
70
  process.stderr.write('Local changes:\n');
72
71
  for (const file of result.local_changes) {
73
- const s = file.additions > 0 && file.deletions > 0 ? 'M' : file.additions > 0 ? 'A' : 'D';
72
+ const s = file.action === 'create' ? 'A' : file.action === 'delete' ? 'D' : 'M';
74
73
  process.stderr.write(` ${s} ${file.path} (+${file.additions} -${file.deletions})\n`);
75
74
  }
76
75
  }
@@ -87,11 +86,6 @@ export function registerWikiCommands(program) {
87
86
  }
88
87
  }
89
88
  }
90
- if (result.permissions) {
91
- process.stderr.write('\nPermissions:\n');
92
- process.stderr.write(` Read: ${result.permissions.readable_prefixes.join(', ')}\n`);
93
- process.stderr.write(` Write: ${result.permissions.writable_prefixes.join(', ') || 'none'}\n`);
94
- }
95
89
  printJson(result);
96
90
  }
97
91
  catch (error) {
@@ -121,7 +115,7 @@ export function registerWikiCommands(program) {
121
115
  });
122
116
  wiki
123
117
  .command('reset')
124
- .description('Discard all local changes and restore files to the last synced state (re-downloads from server).')
118
+ .description('Discard all local changes and restore files to the last synced state.')
125
119
  .argument('[wiki]', 'Wiki ID or slug (auto-resolves if org has one wiki)')
126
120
  .action(async (wikiRef) => {
127
121
  try {
@@ -134,6 +128,23 @@ export function registerWikiCommands(program) {
134
128
  printError(error);
135
129
  }
136
130
  });
131
+ wiki
132
+ .command('access')
133
+ .description('Show your access level (read / maintain / admin) for a wiki path')
134
+ .argument('<path>', 'Path to check (e.g. docs/ops/)')
135
+ .argument('[wiki]', 'Wiki ID or slug (auto-resolves if org has one wiki)')
136
+ .action(async (targetPath, wikiRef) => {
137
+ try {
138
+ const ctx = resolveCredentials();
139
+ const { resolveWikiRefOrDefault } = await import('../lib/wiki.js');
140
+ const wiki = await resolveWikiRefOrDefault(ctx, wikiRef);
141
+ const result = await ctx.client.getWikiAccessStatus(ctx.orgId, wiki.id, targetPath);
142
+ printJson(result);
143
+ }
144
+ catch (error) {
145
+ printError(error);
146
+ }
147
+ });
137
148
  wiki
138
149
  .command('request-access')
139
150
  .description('Request read/write access to a locked wiki path. Creates an approval card for a maintainer.')
@@ -153,9 +164,9 @@ export function registerWikiCommands(program) {
153
164
  });
154
165
  wiki
155
166
  .command('log')
156
- .description('Show version history. With a path: per-file history. Without: recent operations.')
167
+ .description("Show history. With a path: that file's commit history. Without: recent wiki operations.")
157
168
  .argument('[wiki]', 'Wiki ID or slug (auto-resolves if org has one wiki)')
158
- .argument('[path]', 'File path for per-file history')
169
+ .argument('[path]', 'File path for per-file commit history')
159
170
  .action(async (wikiRef, filePath) => {
160
171
  try {
161
172
  const ctx = resolveCredentials();
@@ -166,7 +177,8 @@ export function registerWikiCommands(program) {
166
177
  else {
167
178
  for (const entry of result.entries) {
168
179
  const pathSuffix = entry.path ? ` ${entry.path}` : '';
169
- process.stderr.write(`${entry.id.slice(0, 8)} ${entry.created_at} ${entry.action}${pathSuffix}\n`);
180
+ const who = entry.author_name ?? entry.actor_id ?? '';
181
+ process.stderr.write(`${entry.id.slice(0, 8)} ${entry.created_at} ${entry.action}${who ? ` (${who})` : ''}${pathSuffix}\n`);
170
182
  }
171
183
  }
172
184
  printJson(result);
@@ -345,7 +357,7 @@ export function registerWikiCommands(program) {
345
357
  // ---- Content browsing (search, query, outline, section) ----
346
358
  wiki
347
359
  .command('search')
348
- .description('Search wiki content by keyword')
360
+ .description('Keyword search over wiki sections. Searches your local workspace copy when synced; otherwise fetches from the server.')
349
361
  .argument('<query>', 'Search query')
350
362
  .argument('[wiki]', 'Wiki ID or slug')
351
363
  .option('--path <path>', 'Restrict to a path prefix')
@@ -377,8 +389,8 @@ export function registerWikiCommands(program) {
377
389
  });
378
390
  wiki
379
391
  .command('query')
380
- .description('Tree-aware structured query over wiki sections')
381
- .argument('<query>', 'Natural-language query')
392
+ .description('Heading-aware keyword search that also ranks whole documents. Same lexical scoring as `search` (not semantic), better for multi-word questions.')
393
+ .argument('<query>', 'Query keywords (multi-word supported)')
382
394
  .argument('[wiki]', 'Wiki ID or slug')
383
395
  .option('--path <path>', 'Restrict to a path prefix')
384
396
  .option('--limit <n>', 'Max results (default 5)')
@@ -453,14 +465,18 @@ export function registerWikiCommands(program) {
453
465
  });
454
466
  wiki
455
467
  .command('cat')
456
- .description('Read a wiki page by file path (raw markdown content)')
468
+ .description('Print a wiki file. Reads your local workspace copy when synced (includes your unproposed edits); falls back to the server.')
457
469
  .argument('<path>', 'File path (e.g. docs/auth.md)')
458
470
  .argument('[wiki]', 'Wiki ID or slug')
459
- .action(async (filePath, wikiRef) => {
471
+ .option('--remote', 'Read the server version even when a local workspace copy exists')
472
+ .action(async (filePath, wikiRef, options) => {
460
473
  try {
461
474
  const ctx = resolveCredentials();
462
- const result = await getWikiBlob(ctx, wikiRef, { path: filePath });
463
- process.stderr.write(`${filePath} (${result.size} bytes)\n\n`);
475
+ const result = await getWikiBlob(ctx, wikiRef, {
476
+ path: filePath,
477
+ remote: options.remote,
478
+ });
479
+ process.stderr.write(`${filePath} (${result.size} bytes, source: ${result.source})\n\n`);
464
480
  process.stdout.write(result.content);
465
481
  if (result.content && !result.content.endsWith('\n'))
466
482
  process.stdout.write('\n');
@@ -0,0 +1,9 @@
1
+ export interface ParsedFrontMatter {
2
+ type?: string;
3
+ description?: string;
4
+ tags?: string[];
5
+ }
6
+ /** True when any OKF field was extracted. */
7
+ export declare function hasFrontMatter(fm: ParsedFrontMatter): boolean;
8
+ export declare function parseFrontMatter(lines: string[], frontMatterEnd: number): ParsedFrontMatter;
9
+ //# sourceMappingURL=wiki-frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wiki-frontmatter.d.ts","sourceRoot":"","sources":["../../src/lib/wiki-frontmatter.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,6CAA6C;AAC7C,wBAAgB,cAAc,CAAC,EAAE,EAAE,iBAAiB,GAAG,OAAO,CAE7D;AAQD,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,cAAc,EAAE,MAAM,GAAG,iBAAiB,CAyB3F"}
@@ -0,0 +1,64 @@
1
+ // Frontmatter (OKF-inspired) parsing for wiki markdown files.
2
+ //
3
+ // This mirrors the Go implementation in server/pkg/markdown/section.go — keep
4
+ // the two behaviorally consistent. It lives in its own module (rather than
5
+ // growing the already-oversized wiki.ts) per the repo's file-size discipline.
6
+ import { load as loadYaml } from 'js-yaml';
7
+ /** True when any OKF field was extracted. */
8
+ export function hasFrontMatter(fm) {
9
+ return fm.type !== undefined || fm.description !== undefined || fm.tags !== undefined;
10
+ }
11
+ // parseFrontMatter extracts the optional OKF fields (type, description, tags)
12
+ // from the leading YAML frontmatter block. Permissive by design: only
13
+ // `---`-delimited YAML is parsed (a `+++` TOML block is detected for section
14
+ // boundaries by detectFrontMatterEnd but its fields are not extracted); any
15
+ // parse error / missing field / wrong-typed value degrades to undefined so a
16
+ // malformed field never discards a valid one.
17
+ export function parseFrontMatter(lines, frontMatterEnd) {
18
+ // frontMatterEnd is one past the closing fence; opening fence is line 0 and
19
+ // the closing fence is line frontMatterEnd-1, so a block needs >= 2 lines.
20
+ if (frontMatterEnd < 2 || lines.length === 0) {
21
+ return {};
22
+ }
23
+ if (lines[0].trim() !== '---') {
24
+ return {}; // only YAML frontmatter carries OKF fields
25
+ }
26
+ const body = lines.slice(1, frontMatterEnd - 1).join('\n');
27
+ let raw;
28
+ try {
29
+ raw = loadYaml(body);
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
35
+ return {};
36
+ }
37
+ const obj = raw;
38
+ return {
39
+ type: frontMatterString(obj.type),
40
+ description: frontMatterString(obj.description),
41
+ tags: frontMatterTags(obj.tags),
42
+ };
43
+ }
44
+ function frontMatterString(v) {
45
+ if (typeof v !== 'string') {
46
+ return undefined;
47
+ }
48
+ const s = v.trim();
49
+ return s === '' ? undefined : s;
50
+ }
51
+ function frontMatterTags(v) {
52
+ if (typeof v === 'string') {
53
+ const s = v.trim();
54
+ return s === '' ? undefined : [s];
55
+ }
56
+ if (Array.isArray(v)) {
57
+ const out = v
58
+ .filter((e) => typeof e === 'string')
59
+ .map((e) => e.trim())
60
+ .filter((e) => e !== '');
61
+ return out.length > 0 ? out : undefined;
62
+ }
63
+ return undefined;
64
+ }
@@ -1,4 +1,5 @@
1
1
  import { type ParallClient, type Wiki, type WikiChangeset, type WikiDiff, type WikiDiffFile, type WikiTreeEntry } from '@parall/sdk';
2
+ export { type ParsedFrontMatter, parseFrontMatter } from './wiki-frontmatter.js';
2
3
  export type ParallContext = {
3
4
  client: ParallClient;
4
5
  orgId: string;
@@ -26,6 +27,9 @@ export type WikiNodeSection = {
26
27
  level: number;
27
28
  start_line: number;
28
29
  end_line: number;
30
+ type?: string;
31
+ description?: string;
32
+ tags?: string[];
29
33
  };
30
34
  export type WikiOutlineResult = {
31
35
  wiki_id: string;
@@ -143,10 +147,12 @@ export declare function getWikiTree(ctx: ParallContext, wikiRef?: string, option
143
147
  }>;
144
148
  export declare function getWikiBlob(ctx: ParallContext, wikiRef: string | undefined, options: {
145
149
  path: string;
150
+ remote?: boolean;
146
151
  }): Promise<{
147
152
  wiki: Wiki;
148
153
  content: string;
149
154
  size: number;
155
+ source: WikiContentSource;
150
156
  }>;
151
157
  export declare function getWikiOutline(ctx: ParallContext, wikiRef?: string, options?: {
152
158
  ref?: string;
@@ -166,14 +172,11 @@ export type AfcsStatusResult = {
166
172
  wiki_slug: string;
167
173
  wiki_name: string;
168
174
  mount_path: string;
169
- mode: string;
170
175
  default_ref: string;
171
176
  /** Local workspace changes vs last-synced manifest. */
172
177
  local_changes: WikiDiffFile[];
173
178
  /** Pending changesets created by this agent. */
174
179
  changesets: AfcsChangesetSummary[];
175
- /** Permission summary — null if the API is not yet available. */
176
- permissions: AfcsPermissionSummary | null;
177
180
  };
178
181
  export type AfcsChangesetSummary = {
179
182
  id: string;
@@ -184,10 +187,6 @@ export type AfcsChangesetSummary = {
184
187
  created_at: string;
185
188
  updated_at: string;
186
189
  };
187
- export type AfcsPermissionSummary = {
188
- readable_prefixes: string[];
189
- writable_prefixes: string[];
190
- };
191
190
  export declare function getAfcsStatus(ctx: ParallContext, wikiRef?: string): Promise<AfcsStatusResult>;
192
191
  export type AfcsDiffResult = {
193
192
  wiki_id: string;
@@ -202,12 +201,15 @@ export declare function getAfcsDiff(ctx: ParallContext, wikiRef?: string): Promi
202
201
  export declare function proposeWikiChangeset(ctx: ParallContext, wikiRef: string | undefined, options: WikiProposeOptions): Promise<WikiProposeResult>;
203
202
  /**
204
203
  * Discard all local changes and restore files to the last synced state.
205
- * Reads the local manifest and restores all files from the server.
204
+ * Restores from the local baseline object store (network only as a
205
+ * SHA-verified fallback for pre-objects mounts), so the workspace lands
206
+ * exactly on the manifest baseline — `wiki status` reports clean afterwards.
206
207
  */
207
208
  export declare function resetWikiWorkspace(ctx: ParallContext, wikiRef?: string): Promise<{
208
209
  wiki_id: string;
209
210
  wiki_slug: string;
210
211
  files_restored: number;
212
+ unrestorable_paths: string[];
211
213
  }>;
212
214
  export type AfcsChangesetsResult = {
213
215
  wiki_id: string;
@@ -234,12 +236,14 @@ export type AfcsAccessRequestResult = {
234
236
  };
235
237
  export declare function requestWikiAccess(ctx: ParallContext, wikiRef: string | undefined, targetPath: string, reason?: string): Promise<AfcsAccessRequestResult>;
236
238
  export type AfcsLogEntry = {
237
- type: 'file_version' | 'operation';
239
+ type: 'commit' | 'operation';
238
240
  id: string;
239
241
  path?: string;
240
242
  action: string;
241
- actor_id: string;
242
- version?: number;
243
+ /** Parall actor ID — operation entries only (git commits carry no Parall identity). */
244
+ actor_id?: string;
245
+ /** Git author display name — commit entries only. */
246
+ author_name?: string;
243
247
  created_at: string;
244
248
  };
245
249
  export type AfcsLogResult = {
@@ -249,6 +253,10 @@ export type AfcsLogResult = {
249
253
  path?: string;
250
254
  entries: AfcsLogEntry[];
251
255
  };
256
+ /**
257
+ * Wiki history. Without a path: recent wiki operations (audit log). With a
258
+ * path: that file's commit history from the default branch.
259
+ */
252
260
  export declare function getWikiLog(ctx: ParallContext, wikiRef?: string, filePath?: string): Promise<AfcsLogResult>;
253
261
  export type SyncResult = {
254
262
  /** True iff every wiki's per-path action ran without failure. Conflicts do
@@ -267,11 +275,13 @@ export type SyncResult = {
267
275
  }[];
268
276
  };
269
277
  /**
270
- * List accessible wikis for this org, sync each via REST
271
- * (manifest comparison + bulk download), and remove stale directories.
278
+ * Sync wiki workspaces via REST (manifest comparison + bulk download).
279
+ * Without `wikiRef`: every accessible wiki is synced and stale mount
280
+ * directories are pruned. With `wikiRef`: only that wiki is synced and
281
+ * pruning is skipped — other wikis' workspaces must survive a scoped sync.
272
282
  * Mount path is determined client-side: {mountRoot}/{slug}/
273
283
  */
274
- export declare function syncAllMounts(ctx: ParallContext): Promise<SyncResult>;
284
+ export declare function syncAllMounts(ctx: ParallContext, wikiRef?: string): Promise<SyncResult>;
275
285
  /**
276
286
  * Run {@link syncAllMounts} in a loop, sleeping `intervalSec` between
277
287
  * iterations. Returns a `stop` callback to break the loop.
@@ -370,5 +380,13 @@ export declare function syncSingleWiki(ctx: ParallContext, wiki: import('@parall
370
380
  */
371
381
  export declare function writeConflictMarker(mountRoot: string, sourcePath: string, suffix: string, content: Buffer): Promise<string>;
372
382
  export declare function computeGitBlobSHA(content: Buffer): string;
373
- export {};
383
+ /**
384
+ * Mirror of the server's text classifier (filetype.IsText): content-type
385
+ * magic sniff, valid UTF-8, and no NUL bytes — kept aligned so content the
386
+ * CLI accepts cannot bounce off the server's 422 USE_UPLOAD later. The
387
+ * shared fixture table (test/testdata/text-classifier-cases.json) is
388
+ * verified by both this implementation's tests and the Go package's tests.
389
+ */
390
+ export declare function isWikiTextContent(content: Buffer): boolean;
391
+ export declare function detectFrontMatterEnd(lines: string[]): number;
374
392
  //# sourceMappingURL=wiki.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"wiki.d.ts","sourceRoot":"","sources":["../../src/lib/wiki.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"wiki.d.ts","sourceRoot":"","sources":["../../src/lib/wiki.ts"],"names":[],"mappings":"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"}
package/dist/lib/wiki.js CHANGED
@@ -2,17 +2,28 @@ import { createHash } from 'node:crypto';
2
2
  import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { ApiError, } from '@parall/sdk';
5
+ import { structuredPatch } from 'diff';
6
+ import { hasFrontMatter, parseFrontMatter } from './wiki-frontmatter.js';
7
+ // Re-exported so consumers (and tests) can reach the frontmatter parser via the
8
+ // wiki module surface alongside detectFrontMatterEnd.
9
+ export { parseFrontMatter } from './wiki-frontmatter.js';
5
10
  // ---------------------------------------------------------------------------
6
11
  // Constants
7
12
  // ---------------------------------------------------------------------------
8
13
  const MARKDOWN_HEADING_RE = /^\s{0,3}(#{1,6})[ \t]+(.+?)\s*$/;
9
14
  const TRAILING_FENCE_RE = /\s+#+\s*$/;
10
- const LOCAL_NODE_SECTION_ARTIFACT_VERSION = 2;
15
+ // Bumped 2 → 3 for the frontmatter (type/description/tags) fields: invalidates
16
+ // caches built before frontmatter parsing so existing local mounts re-parse and
17
+ // pick up the metadata even when a file's size / mtime is unchanged.
18
+ const LOCAL_NODE_SECTION_ARTIFACT_VERSION = 3;
11
19
  const LOCAL_MANIFEST_FILE = 'manifest.json';
12
20
  const PRLL_WIKI_DIR = '.parall-wiki';
13
21
  const CONFLICTS_DIR = 'conflicts';
22
+ const OBJECTS_DIR = 'objects';
14
23
  const REMOTE_CONFLICT_SUFFIX = '.remote';
15
24
  const REMOTE_DELETED_MARKER_SUFFIX = '.remote-deleted';
25
+ /** Server-side text ceiling for changeset file content (mirrors filetype.TextMaxBytes). */
26
+ const TEXT_MAX_BYTES = 10 * 1024 * 1024;
16
27
  // ---------------------------------------------------------------------------
17
28
  // Wiki resolution helpers
18
29
  // ---------------------------------------------------------------------------
@@ -34,7 +45,10 @@ export async function resolveWikiRef(ctx, wikiRef) {
34
45
  if (fuzzy) {
35
46
  return fuzzy;
36
47
  }
37
- throw new Error(`wiki not found: ${wikiRef}`);
48
+ const available = wikis.length > 0
49
+ ? ` Available wikis: ${wikis.map((w) => w.slug).join(', ')}`
50
+ : ' This organization has no wikis yet.';
51
+ throw new Error(`wiki not found: ${wikiRef}.${available}`);
38
52
  }
39
53
  /**
40
54
  * Resolve wiki reference. If wikiRef is omitted or empty, auto-resolves to
@@ -70,22 +84,55 @@ export async function getWikiTree(ctx, wikiRef, options = {}) {
70
84
  }
71
85
  export async function getWikiBlob(ctx, wikiRef, options) {
72
86
  const wiki = await resolveWikiRef(ctx, wikiRef);
87
+ const normalizedPath = normalizeRequiredWikiPath(options.path);
88
+ // Local mount first (same data source as search/section/outline) so reads
89
+ // reflect the agent's own in-progress edits. Files absent locally AND
90
+ // absent from the manifest (never synced, or ACL-filtered) fall through to
91
+ // the API; files in the manifest but missing on disk are pending local
92
+ // deletes — surfacing the server copy there would make a delete look like
93
+ // it failed.
94
+ if (!options.remote) {
95
+ const mount = await resolveLocalWikiMount(ctx, wiki);
96
+ if (mount) {
97
+ const target = resolvePathInsideRoot(mount.root, normalizedPath);
98
+ const stat = await fs.stat(target).catch(() => null);
99
+ if (stat?.isFile()) {
100
+ const content = await fs.readFile(target);
101
+ if (looksBinary(content)) {
102
+ throw new Error(`wiki file is binary, not printable text: ${normalizedPath}. ` +
103
+ 'Binary files are viewable in the web UI.');
104
+ }
105
+ return {
106
+ wiki,
107
+ content: content.toString('utf8'),
108
+ size: content.length,
109
+ source: 'local_mount',
110
+ };
111
+ }
112
+ const manifest = await readLocalManifest(mount.root);
113
+ if (manifest.files[normalizedPath]) {
114
+ throw new Error(`${normalizedPath} is deleted in your local workspace (pending delete). ` +
115
+ 'Run `parall wiki changeset create` to propose the deletion, ' +
116
+ '`parall wiki reset` to restore it, or `parall wiki cat --remote` to read the server version.');
117
+ }
118
+ }
119
+ }
73
120
  const blob = await ctx.client.getWikiBlob(ctx.orgId, wiki.id, {
74
- path: normalizeRequiredWikiPath(options.path),
121
+ path: normalizedPath,
75
122
  });
76
123
  // Phase 2 widened WikiBlob to allow encoding='signed_url' (binary served
77
124
  // via preview URL) and still admits the legacy 'base64' inline shape,
78
125
  // both of which omit or encode `content` in ways the CLI's string surface
79
- // cannot use. Only utf-8 text is safe to return verbatim; anything else
80
- // gets a clear error — callers that want bytes should fetch via the
81
- // signed_url flow.
126
+ // cannot use. Only utf-8 text is safe to return verbatim.
82
127
  if (blob.encoding !== 'utf-8' || blob.content == null) {
83
- throw new Error(`wiki blob is not inline utf-8 (encoding=${blob.encoding}): ${options.path}`);
128
+ throw new Error(`wiki file is binary, not printable text (encoding=${blob.encoding}): ${normalizedPath}. ` +
129
+ 'Binary files are viewable in the web UI.');
84
130
  }
85
131
  return {
86
132
  wiki,
87
133
  content: blob.content,
88
134
  size: blob.size,
135
+ source: 'api',
89
136
  };
90
137
  }
91
138
  // ---------------------------------------------------------------------------
@@ -259,20 +306,17 @@ export async function getAfcsStatus(ctx, wikiRef) {
259
306
  catch {
260
307
  // Changesets API may fail — non-fatal for status
261
308
  }
262
- const permissions = {
263
- readable_prefixes: ['(all server ACL)'],
264
- writable_prefixes: ['(server ACL)'],
265
- };
309
+ // No permissions block: per-path access is queryable via `parall wiki
310
+ // access <path>`; a whole-tree writability summary needs the path-scope
311
+ // semantics still moving in the restriction-layer work.
266
312
  return {
267
313
  wiki_id: wiki.id,
268
314
  wiki_slug: wiki.slug,
269
315
  wiki_name: wiki.name,
270
316
  mount_path: mount.root,
271
- mode: 'read_write',
272
317
  default_ref: wiki.default_branch,
273
318
  local_changes: localChanges,
274
319
  changesets,
275
- permissions,
276
320
  };
277
321
  }
278
322
  export async function getAfcsDiff(ctx, wikiRef) {
@@ -295,7 +339,7 @@ export async function getAfcsDiff(ctx, wikiRef) {
295
339
  // ---------------------------------------------------------------------------
296
340
  export async function proposeWikiChangeset(ctx, wikiRef, options) {
297
341
  const wiki = await resolveWikiRef(ctx, wikiRef);
298
- const mount = await requireLocalWikiMount(ctx, wiki, { requireWrite: true });
342
+ const mount = await requireLocalWikiMount(ctx, wiki);
299
343
  const title = options.title.trim();
300
344
  if (!title) {
301
345
  throw new Error('title is required');
@@ -303,7 +347,38 @@ export async function proposeWikiChangeset(ctx, wikiRef, options) {
303
347
  const manifest = await readLocalManifest(mount.root);
304
348
  const diff = await computeLocalDiff(mount, manifest);
305
349
  if (diff.changedPaths.length === 0) {
306
- throw new Error(`no changes to propose for wiki ${wiki.slug}`);
350
+ throw new Error(`no changes to propose for wiki ${wiki.slug} — edit files under ${mount.root} first, ` +
351
+ 'then run `parall wiki diff` to confirm what will be proposed');
352
+ }
353
+ // Stale-base precheck (fail-closed): compare each changed path's sync
354
+ // baseline against the server's CURRENT manifest. Proposing from a stale
355
+ // baseline would upload full content that silently rolls back concurrent
356
+ // edits. The server re-validates via base_sha (authoritative — this check
357
+ // just fails earlier with the complete file list).
358
+ const serverManifest = (await wikiServiceFetch(mount.manifestUrl, requireMountToken(mount)));
359
+ const serverShaByPath = new Map(serverManifest.data.map((e) => [e.path, e.sha]));
360
+ const staleLines = [];
361
+ for (const filePath of diff.changedPaths) {
362
+ const baseSha = diff.baseShaByPath.get(filePath);
363
+ const serverSha = serverShaByPath.get(filePath);
364
+ if (baseSha === undefined) {
365
+ // Locally created file — stale if the server got one in the meantime.
366
+ if (serverSha !== undefined) {
367
+ staleLines.push(`${filePath} (created on the server since your last sync)`);
368
+ }
369
+ }
370
+ else if (serverSha === undefined) {
371
+ staleLines.push(`${filePath} (deleted on the server since your last sync)`);
372
+ }
373
+ else if (serverSha !== baseSha) {
374
+ staleLines.push(`${filePath} (modified on the server since your last sync)`);
375
+ }
376
+ }
377
+ if (staleLines.length > 0) {
378
+ throw new Error(`cannot propose: ${staleLines.length} file(s) changed on the server since your last sync:\n` +
379
+ staleLines.map((l) => ` ${l}`).join('\n') +
380
+ '\nRun `parall wiki sync` to pull the latest content (your local edits are preserved; ' +
381
+ 'conflicting files get a marker under .parall-wiki/conflicts/), resolve if needed, then propose again.');
307
382
  }
308
383
  // Build file_changes for the changeset API
309
384
  const fileChanges = [];
@@ -316,25 +391,43 @@ export async function proposeWikiChangeset(ctx, wikiRef, options) {
316
391
  fileChanges.push({
317
392
  path: filePath,
318
393
  action: 'delete',
394
+ base_sha: baseEntry?.sha,
319
395
  });
320
396
  }
321
397
  else {
322
398
  const content = await fs.readFile(localPath);
399
+ // The changeset API is text-only (the server rejects binary with 422
400
+ // USE_UPLOAD, pointing at an endpoint the CLI does not wrap). Reject
401
+ // locally with a clear boundary statement instead.
402
+ if (looksBinary(content)) {
403
+ throw new Error(`cannot propose ${filePath}: wiki changesets support text files only. ` +
404
+ 'Remove the binary from the workspace (binary uploads go through the web UI).');
405
+ }
406
+ if (content.length > TEXT_MAX_BYTES) {
407
+ throw new Error(`cannot propose ${filePath}: file is ${content.length} bytes, ` +
408
+ `over the ${TEXT_MAX_BYTES}-byte changeset limit. Split the file or remove it from the workspace.`);
409
+ }
323
410
  fileChanges.push({
324
411
  path: filePath,
325
412
  action: baseEntry ? 'update' : 'create',
326
413
  content_base64: content.toString('base64'),
414
+ base_sha: baseEntry?.sha,
327
415
  });
328
416
  }
329
417
  }
330
418
  let changeset;
331
419
  const existingChangesetId = normalizeOptionalString(options.changesetId);
332
420
  if (existingChangesetId) {
333
- // Re-propose existing changeset with updated file_changes
421
+ // Re-propose: replace_files makes the workspace diff define the
422
+ // changeset's FULL content — the server resets the feature branch to
423
+ // default HEAD first, so a change withdrawn locally (file reverted to
424
+ // baseline) actually disappears from the proposal instead of silently
425
+ // surviving on the branch.
334
426
  changeset = await ctx.client.updateWikiChangeset(ctx.orgId, wiki.id, existingChangesetId, {
335
427
  title,
336
428
  message: normalizeOptionalString(options.message),
337
429
  file_changes: fileChanges,
430
+ replace_files: true,
338
431
  });
339
432
  }
340
433
  else {
@@ -407,34 +500,52 @@ function describeProposeNextAction(changeset, postMergeSync, postMergeSyncError)
407
500
  // ---------------------------------------------------------------------------
408
501
  /**
409
502
  * Discard all local changes and restore files to the last synced state.
410
- * Reads the local manifest and restores all files from the server.
503
+ * Restores from the local baseline object store (network only as a
504
+ * SHA-verified fallback for pre-objects mounts), so the workspace lands
505
+ * exactly on the manifest baseline — `wiki status` reports clean afterwards.
411
506
  */
412
507
  export async function resetWikiWorkspace(ctx, wikiRef) {
413
508
  const wiki = await resolveWikiRef(ctx, wikiRef);
414
509
  const mount = await requireLocalWikiMount(ctx, wiki);
415
510
  const manifest = await readLocalManifest(mount.root);
416
- // Count changes before reset
511
+ // Only touch files that actually diverged from the baseline.
417
512
  const changes = await computeLocalChanges(mount);
418
- const changedCount = changes.length;
419
- if (changedCount === 0) {
420
- return { wiki_id: wiki.id, wiki_slug: wiki.slug, files_restored: 0 };
421
- }
422
- // Re-download all manifest files and overwrite local state
423
- const manifestPaths = Object.keys(manifest.files);
424
- if (manifestPaths.length > 0) {
425
- const downloaded = await bulkDownloadFiles(mount.bulkDownloadUrl, requireMountToken(mount), manifestPaths);
426
- for (const entry of downloaded) {
427
- const fullPath = resolvePathInsideRoot(mount.root, entry.path);
428
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
429
- await fs.writeFile(fullPath, Buffer.from(entry.content_base64, 'base64'));
513
+ if (changes.length === 0) {
514
+ return { wiki_id: wiki.id, wiki_slug: wiki.slug, files_restored: 0, unrestorable_paths: [] };
515
+ }
516
+ let restored = 0;
517
+ const unrestorable = [];
518
+ for (const change of changes) {
519
+ const entry = manifest.files[change.path];
520
+ const fullPath = resolvePathInsideRoot(mount.root, change.path);
521
+ if (!entry) {
522
+ // Locally created file — discard.
523
+ await fs.rm(fullPath, { force: true });
524
+ await removeEmptyParents(mount.root, path.dirname(fullPath));
525
+ restored++;
526
+ continue;
527
+ }
528
+ const baseline = await loadBaselineContent(mount, change.path, entry.sha);
529
+ if (!baseline) {
530
+ // No baseline available (pre-objects mount, server moved on). Leave the
531
+ // local file untouched rather than overwriting it with non-baseline
532
+ // content; `parall wiki sync` is the path that reconciles this state.
533
+ unrestorable.push(change.path);
534
+ continue;
430
535
  }
536
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
537
+ await fs.writeFile(fullPath, baseline);
538
+ restored++;
539
+ }
540
+ if (unrestorable.length > 0) {
541
+ console.warn(`[wiki reset] ${unrestorable.length} file(s) had no recoverable baseline and were left as-is: ` +
542
+ `${unrestorable.join(', ')} — run \`parall wiki sync\` to reconcile`);
431
543
  }
432
- // Remove any local file not in manifest
433
- await removeFilesNotInManifest(mount.root, manifest);
434
544
  return {
435
545
  wiki_id: wiki.id,
436
546
  wiki_slug: wiki.slug,
437
- files_restored: changedCount,
547
+ files_restored: restored,
548
+ unrestorable_paths: unrestorable,
438
549
  };
439
550
  }
440
551
  export async function listWikiChangesets(ctx, wikiRef) {
@@ -495,10 +606,32 @@ export async function requestWikiAccess(ctx, wikiRef, targetPath, reason) {
495
606
  message: 'Access request submitted. Waiting for approval.',
496
607
  };
497
608
  }
609
+ /**
610
+ * Wiki history. Without a path: recent wiki operations (audit log). With a
611
+ * path: that file's commit history from the default branch.
612
+ */
498
613
  export async function getWikiLog(ctx, wikiRef, filePath) {
499
614
  const wiki = await resolveWikiRef(ctx, wikiRef);
500
- if (filePath?.trim()) {
501
- throw new Error('Per-file wiki history is not implemented yet. Omit the path to see org-wide operations.');
615
+ // Same normalization as every other path entry point — `./docs/a.md` or
616
+ // `/docs/a.md` would otherwise reach the server un-normalized and read
617
+ // back as an empty history.
618
+ const normalizedPath = filePath?.trim() ? normalizeRequiredWikiPath(filePath) : undefined;
619
+ if (normalizedPath) {
620
+ const commits = await ctx.client.getWikiFileCommits(ctx.orgId, wiki.id, normalizedPath);
621
+ return {
622
+ wiki_id: wiki.id,
623
+ wiki_slug: wiki.slug,
624
+ wiki_name: wiki.name,
625
+ path: normalizedPath,
626
+ entries: commits.data.map((c) => ({
627
+ type: 'commit',
628
+ id: c.sha,
629
+ path: normalizedPath,
630
+ action: c.title,
631
+ author_name: c.author_name,
632
+ created_at: c.date,
633
+ })),
634
+ };
502
635
  }
503
636
  const ops = await ctx.client.getWikiOperations(ctx.orgId, wiki.id);
504
637
  const entries = ops.data.map((op) => ({
@@ -516,12 +649,16 @@ export async function getWikiLog(ctx, wikiRef, filePath) {
516
649
  };
517
650
  }
518
651
  /**
519
- * List accessible wikis for this org, sync each via REST
520
- * (manifest comparison + bulk download), and remove stale directories.
652
+ * Sync wiki workspaces via REST (manifest comparison + bulk download).
653
+ * Without `wikiRef`: every accessible wiki is synced and stale mount
654
+ * directories are pruned. With `wikiRef`: only that wiki is synced and
655
+ * pruning is skipped — other wikis' workspaces must survive a scoped sync.
521
656
  * Mount path is determined client-side: {mountRoot}/{slug}/
522
657
  */
523
- export async function syncAllMounts(ctx) {
524
- const wikis = await ctx.client.getWikis(ctx.orgId);
658
+ export async function syncAllMounts(ctx, wikiRef) {
659
+ const allWikis = await ctx.client.getWikis(ctx.orgId);
660
+ const target = wikiRef?.trim() ? await resolveWikiRef(ctx, wikiRef) : undefined;
661
+ const wikis = target ? allWikis.filter((w) => w.id === target.id) : allWikis;
525
662
  const mountRoot = ctx.mountRoot?.trim() || process.env.PRLL_WIKI_MOUNT_ROOT?.trim();
526
663
  const activeMountPaths = new Set();
527
664
  const synced = [];
@@ -538,7 +675,9 @@ export async function syncAllMounts(ctx) {
538
675
  failed: outcome.failed,
539
676
  });
540
677
  }
541
- await pruneStaleMounts(mountRoot, activeMountPaths);
678
+ if (!target) {
679
+ await pruneStaleMounts(mountRoot, activeMountPaths);
680
+ }
542
681
  const ok = synced.every((s) => s.failed.length === 0);
543
682
  return { ok, mounts: wikis.length, synced };
544
683
  }
@@ -695,6 +834,7 @@ export async function syncSingleWiki(ctx, wiki, mountPath) {
695
834
  }
696
835
  // 5. Apply actions and start with the previous manifest, mutating per action.
697
836
  const newFiles = { ...localManifest.files };
837
+ const downloadedBySha = new Map();
698
838
  const conflicts = [];
699
839
  const failed = [];
700
840
  let applied = 0;
@@ -763,6 +903,7 @@ export async function syncSingleWiki(ctx, wiki, mountPath) {
763
903
  await fs.mkdir(path.dirname(fullPath), { recursive: true });
764
904
  await fs.writeFile(fullPath, buf);
765
905
  newFiles[action.path] = { sha: action.remoteSha, size: action.remoteSize };
906
+ downloadedBySha.set(action.remoteSha, buf);
766
907
  applied++;
767
908
  break;
768
909
  }
@@ -853,13 +994,16 @@ export async function syncSingleWiki(ctx, wiki, mountPath) {
853
994
  }
854
995
  }
855
996
  }
856
- // 6. Persist new manifest.
997
+ // 6. Persist new manifest + refresh the baseline object store so diff and
998
+ // reset can always reproduce the exact synced content locally.
857
999
  const newManifest = {
858
1000
  wiki_id: wiki.id,
859
1001
  synced_at: new Date().toISOString(),
860
1002
  files: newFiles,
861
1003
  };
862
1004
  await writeLocalManifest(mountPath, newManifest);
1005
+ await ensureBaselineObjects(mountPath, newFiles, downloadedBySha);
1006
+ await gcBaselineObjects(mountPath, newFiles);
863
1007
  if (conflicts.length > 0) {
864
1008
  for (const c of conflicts) {
865
1009
  const where = c.remoteCopyPath ? ` → ${path.relative(mountPath, c.remoteCopyPath)}` : '';
@@ -949,6 +1093,104 @@ async function writeLocalManifest(mountRoot, manifest) {
949
1093
  await fs.writeFile(path.join(metaDir, LOCAL_MANIFEST_FILE), JSON.stringify(manifest, null, 2), 'utf8');
950
1094
  }
951
1095
  // ---------------------------------------------------------------------------
1096
+ // Baseline object store (.parall-wiki/objects/<sha>)
1097
+ //
1098
+ // Content-addressed copies of the last-synced version of every manifest file.
1099
+ // diff/status/reset read the true sync baseline from here instead of fetching
1100
+ // the server's CURRENT content (which is the wrong base whenever the server
1101
+ // has moved past the local sync point).
1102
+ // ---------------------------------------------------------------------------
1103
+ function baselineObjectPath(mountRoot, sha) {
1104
+ return path.join(mountRoot, PRLL_WIKI_DIR, OBJECTS_DIR, sha);
1105
+ }
1106
+ async function readBaselineObject(mountRoot, sha) {
1107
+ try {
1108
+ return await fs.readFile(baselineObjectPath(mountRoot, sha));
1109
+ }
1110
+ catch {
1111
+ return null;
1112
+ }
1113
+ }
1114
+ async function writeBaselineObject(mountRoot, sha, content) {
1115
+ const target = baselineObjectPath(mountRoot, sha);
1116
+ await fs.mkdir(path.dirname(target), { recursive: true });
1117
+ await fs.writeFile(target, content);
1118
+ }
1119
+ /**
1120
+ * Make sure every manifest entry has its baseline object on disk.
1121
+ * Content sources, in order: bytes downloaded during this sync
1122
+ * (`contentBySha`), then the local working-tree file when its blob SHA still
1123
+ * matches the manifest (clean file — typical for pre-objects mounts being
1124
+ * upgraded). Files that are locally dirty AND missing an object stay absent;
1125
+ * diff falls back per-file (see loadBaselineContent).
1126
+ */
1127
+ async function ensureBaselineObjects(mountRoot, files, contentBySha) {
1128
+ for (const [relPath, entry] of Object.entries(files)) {
1129
+ if (await pathExists(baselineObjectPath(mountRoot, entry.sha))) {
1130
+ continue;
1131
+ }
1132
+ const downloaded = contentBySha?.get(entry.sha);
1133
+ if (downloaded) {
1134
+ await writeBaselineObject(mountRoot, entry.sha, downloaded);
1135
+ continue;
1136
+ }
1137
+ const probe = await probeLocalGitBlob(mountRoot, relPath);
1138
+ if (probe.kind === 'file' && probe.sha === entry.sha) {
1139
+ const fullPath = resolvePathInsideRoot(mountRoot, relPath);
1140
+ await writeBaselineObject(mountRoot, entry.sha, await fs.readFile(fullPath));
1141
+ }
1142
+ }
1143
+ }
1144
+ /** Drop baseline objects no longer referenced by any manifest entry. */
1145
+ async function gcBaselineObjects(mountRoot, files) {
1146
+ const objectsDir = path.join(mountRoot, PRLL_WIKI_DIR, OBJECTS_DIR);
1147
+ let names;
1148
+ try {
1149
+ names = await fs.readdir(objectsDir);
1150
+ }
1151
+ catch {
1152
+ return;
1153
+ }
1154
+ const live = new Set(Object.values(files).map((e) => e.sha));
1155
+ for (const name of names) {
1156
+ if (!live.has(name)) {
1157
+ await fs.rm(path.join(objectsDir, name), { force: true });
1158
+ }
1159
+ }
1160
+ }
1161
+ /**
1162
+ * Load the baseline (last-synced) content for a manifest entry. Objects store
1163
+ * first; when absent (mount predates the objects store and the file is
1164
+ * locally dirty), fall back to downloading the server's current content and
1165
+ * accept it ONLY if its blob SHA matches the manifest base — otherwise the
1166
+ * server has moved and no true baseline is available (returns null).
1167
+ */
1168
+ async function loadBaselineContent(mount, relPath, expectedSha) {
1169
+ const cached = await readBaselineObject(mount.root, expectedSha);
1170
+ if (cached) {
1171
+ return cached;
1172
+ }
1173
+ if (!mount.token) {
1174
+ return null;
1175
+ }
1176
+ try {
1177
+ const entries = await bulkDownloadFiles(mount.bulkDownloadUrl, mount.token, [relPath]);
1178
+ const entry = entries[0];
1179
+ if (!entry || entry.error) {
1180
+ return null;
1181
+ }
1182
+ const content = Buffer.from(entry.content_base64, 'base64');
1183
+ if (computeGitBlobSHA(content) !== expectedSha) {
1184
+ return null;
1185
+ }
1186
+ await writeBaselineObject(mount.root, expectedSha, content);
1187
+ return content;
1188
+ }
1189
+ catch {
1190
+ return null;
1191
+ }
1192
+ }
1193
+ // ---------------------------------------------------------------------------
952
1194
  // REST helpers for wiki-service
953
1195
  // ---------------------------------------------------------------------------
954
1196
  function resolveApiToken(ctx) {
@@ -1011,54 +1253,92 @@ export function computeGitBlobSHA(content) {
1011
1253
  const header = Buffer.from(`blob ${content.length}\0`, 'utf8');
1012
1254
  return createHash('sha1').update(header).update(content).digest('hex');
1013
1255
  }
1014
- /** Compute list of changed files by comparing local disk to local manifest. */
1015
- async function computeLocalChanges(mount) {
1016
- const manifest = await readLocalManifest(mount.root);
1017
- const diffFiles = [];
1018
- const visited = new Set();
1019
- // Walk local files to find modified/added
1020
- await walkLocalFiles(mount.root, mount.root, async (fullPath, relativePath) => {
1021
- if (!matchesAllowedPrefix(relativePath, [])) {
1022
- return;
1023
- }
1024
- visited.add(relativePath);
1025
- const localContent = await fs.readFile(fullPath);
1026
- const localSha = computeGitBlobSHA(localContent);
1027
- const manifestEntry = manifest.files[relativePath];
1028
- if (!manifestEntry) {
1029
- // New file
1030
- const lines = localContent.toString('utf8').split('\n').length;
1031
- diffFiles.push({ path: relativePath, additions: lines, deletions: 0 });
1032
- }
1033
- else if (localSha !== manifestEntry.sha) {
1034
- // Modified file compute line diff estimate
1035
- const localLines = localContent.toString('utf8').split('\n');
1036
- const additions = localLines.length;
1037
- const deletions = estimateLines(manifestEntry.size);
1038
- diffFiles.push({ path: relativePath, additions, deletions });
1256
+ // Exact mirror of the magic signatures where the server's classifier
1257
+ // (filetype.IsText → Go http.DetectContentType → isBinaryContentType)
1258
+ // rejects content that could still be NUL-free, UTF-8-decodable bytes.
1259
+ // Signatures whose match requires NUL or non-UTF-8 bytes (PNG, JPEG, OGG,
1260
+ // MIDI, ICO, …) are deliberately omitted — the UTF-8 + NUL checks below
1261
+ // already cover them. Keep this table in lockstep with the Go sniffing
1262
+ // rules; test/testdata/text-classifier-cases.json is the shared contract
1263
+ // both sides verify against.
1264
+ const BINARY_MAGIC_PREFIXES = [
1265
+ Buffer.from('GIF87a'), // image/gif
1266
+ Buffer.from('GIF89a'), // image/gif
1267
+ Buffer.from('%PDF-'), // application/pdf
1268
+ Buffer.from('BM'), // image/bmp (yes, bare "BM" — Go sniffs it as BMP)
1269
+ Buffer.from('ID3'), // audio/mpeg
1270
+ Buffer.from([0x50, 0x4b, 0x03, 0x04]), // application/zip
1271
+ Buffer.from([0x1f, 0x8b, 0x08]), // application/x-gzip
1272
+ ];
1273
+ // Container formats Go sniffs via offset-masked signatures: 4-byte container
1274
+ // tag at offset 0, format tag at offset 8.
1275
+ const BINARY_CONTAINER_SIGS = [
1276
+ { head: 'RIFF', at8: 'WAVE' }, // audio/wave
1277
+ { head: 'RIFF', at8: 'AVI ' }, // video/avi
1278
+ { head: 'RIFF', at8: 'WEBPVP' }, // image/webp
1279
+ { head: 'FORM', at8: 'AIFF' }, // audio/aiff
1280
+ ];
1281
+ const utf8Strict = new TextDecoder('utf-8', { fatal: true });
1282
+ /**
1283
+ * Mirror of the server's text classifier (filetype.IsText): content-type
1284
+ * magic sniff, valid UTF-8, and no NUL bytes — kept aligned so content the
1285
+ * CLI accepts cannot bounce off the server's 422 USE_UPLOAD later. The
1286
+ * shared fixture table (test/testdata/text-classifier-cases.json) is
1287
+ * verified by both this implementation's tests and the Go package's tests.
1288
+ */
1289
+ export function isWikiTextContent(content) {
1290
+ for (const magic of BINARY_MAGIC_PREFIXES) {
1291
+ if (content.subarray(0, magic.length).equals(magic)) {
1292
+ return false;
1039
1293
  }
1040
- });
1041
- // Find deleted files (in manifest but not on disk)
1042
- for (const [filePath, entry] of Object.entries(manifest.files)) {
1043
- if (!visited.has(filePath)) {
1044
- const deletions = estimateLines(entry.size);
1045
- diffFiles.push({ path: filePath, additions: 0, deletions });
1294
+ }
1295
+ for (const sig of BINARY_CONTAINER_SIGS) {
1296
+ if (content.subarray(0, 4).equals(Buffer.from(sig.head)) &&
1297
+ content.subarray(8, 8 + sig.at8.length).equals(Buffer.from(sig.at8))) {
1298
+ return false;
1046
1299
  }
1047
1300
  }
1048
- diffFiles.sort((a, b) => a.path.localeCompare(b.path));
1049
- return diffFiles;
1301
+ if (content.includes(0)) {
1302
+ return false;
1303
+ }
1304
+ try {
1305
+ utf8Strict.decode(content);
1306
+ }
1307
+ catch {
1308
+ return false;
1309
+ }
1310
+ return true;
1050
1311
  }
1051
- /** Compute diff including unified patch by comparing local files vs manifest base content. */
1312
+ function looksBinary(content) {
1313
+ return !isWikiTextContent(content);
1314
+ }
1315
+ /** Compute list of changed files by comparing local disk to local manifest. */
1316
+ async function computeLocalChanges(mount) {
1317
+ const manifest = await readLocalManifest(mount.root);
1318
+ const diff = await computeLocalDiff(mount, manifest);
1319
+ return diff.diffFiles;
1320
+ }
1321
+ /**
1322
+ * Compute the local workspace diff against the last-synced baseline. Base
1323
+ * content comes from the local objects store (with a SHA-verified network
1324
+ * fallback for pre-objects mounts), so the diff base is always the true sync
1325
+ * point — never the server's current content.
1326
+ */
1052
1327
  async function computeLocalDiff(mount, manifest) {
1053
1328
  const diffFiles = [];
1054
1329
  const patchParts = [];
1055
1330
  const visited = new Set();
1056
- const token = requireMountToken(mount);
1331
+ const baseShaByPath = new Map();
1332
+ const loadBase = async (relPath, entry) => {
1333
+ const base = await loadBaselineContent(mount, relPath, entry.sha);
1334
+ // A missing baseline (pre-objects mount with a locally dirty file and a
1335
+ // server that has since moved) degrades to an empty base: the patch shows
1336
+ // the full local content as additions. propose() still carries the right
1337
+ // base_sha, so staleness is caught server-side regardless.
1338
+ return base ? base.toString('utf8') : '';
1339
+ };
1057
1340
  // Walk local files to find modified/added
1058
1341
  await walkLocalFiles(mount.root, mount.root, async (fullPath, relativePath) => {
1059
- if (!matchesAllowedPrefix(relativePath, [])) {
1060
- return;
1061
- }
1062
1342
  visited.add(relativePath);
1063
1343
  const localContent = await fs.readFile(fullPath);
1064
1344
  const localSha = computeGitBlobSHA(localContent);
@@ -1066,83 +1346,62 @@ async function computeLocalDiff(mount, manifest) {
1066
1346
  if (!manifestEntry) {
1067
1347
  // New file
1068
1348
  const localText = localContent.toString('utf8');
1069
- const localLines = localText.split('\n');
1070
- diffFiles.push({ path: relativePath, additions: localLines.length, deletions: 0 });
1071
- patchParts.push(buildUnifiedDiff(relativePath, '', localText));
1349
+ appendFileDiff(diffFiles, patchParts, relativePath, '', localText, 'create');
1072
1350
  }
1073
1351
  else if (localSha !== manifestEntry.sha) {
1074
- // Modified file — fetch base content from server for diff
1075
- const localText = localContent.toString('utf8');
1076
- let baseText = '';
1077
- try {
1078
- const baseEntries = await bulkDownloadFiles(mount.bulkDownloadUrl, token, [relativePath]);
1079
- if (baseEntries.length > 0) {
1080
- baseText = Buffer.from(baseEntries[0].content_base64, 'base64').toString('utf8');
1081
- }
1082
- }
1083
- catch {
1084
- // If we can't fetch base, treat as full addition
1085
- }
1086
- const localLines = localText.split('\n');
1087
- const baseLines = baseText.split('\n');
1088
- diffFiles.push({
1089
- path: relativePath,
1090
- additions: localLines.length,
1091
- deletions: baseLines.length,
1092
- });
1093
- patchParts.push(buildUnifiedDiff(relativePath, baseText, localText));
1352
+ // Modified file
1353
+ baseShaByPath.set(relativePath, manifestEntry.sha);
1354
+ const baseText = await loadBase(relativePath, manifestEntry);
1355
+ appendFileDiff(diffFiles, patchParts, relativePath, baseText, localContent.toString('utf8'), 'update');
1094
1356
  }
1095
1357
  });
1096
1358
  // Find deleted files
1097
1359
  for (const [filePath, entry] of Object.entries(manifest.files)) {
1098
1360
  if (!visited.has(filePath)) {
1099
- let baseText = '';
1100
- try {
1101
- const baseEntries = await bulkDownloadFiles(mount.bulkDownloadUrl, token, [filePath]);
1102
- if (baseEntries.length > 0) {
1103
- baseText = Buffer.from(baseEntries[0].content_base64, 'base64').toString('utf8');
1104
- }
1105
- }
1106
- catch {
1107
- // Estimate
1108
- }
1109
- const baseLines = baseText.split('\n');
1110
- diffFiles.push({ path: filePath, additions: 0, deletions: baseLines.length });
1111
- patchParts.push(buildUnifiedDiff(filePath, baseText, ''));
1361
+ baseShaByPath.set(filePath, entry.sha);
1362
+ const baseText = await loadBase(filePath, entry);
1363
+ appendFileDiff(diffFiles, patchParts, filePath, baseText, '', 'delete');
1112
1364
  }
1113
1365
  }
1114
1366
  diffFiles.sort((a, b) => a.path.localeCompare(b.path));
1115
1367
  const changedPaths = diffFiles.map((f) => f.path);
1116
1368
  const patch = patchParts.join('');
1117
- return { changedPaths, diffFiles, patch };
1118
- }
1119
- /** Build a minimal unified diff between two text strings. */
1120
- function buildUnifiedDiff(filePath, oldText, newText) {
1121
- const oldLines = oldText ? oldText.split('\n') : [];
1122
- const newLines = newText ? newText.split('\n') : [];
1123
- const aPath = oldText ? `a/${filePath}` : '/dev/null';
1124
- const bPath = newText ? `b/${filePath}` : '/dev/null';
1369
+ return { changedPaths, diffFiles, patch, baseShaByPath };
1370
+ }
1371
+ /** Append one file's real (Myers) unified diff and +/- counts. */
1372
+ function appendFileDiff(diffFiles, patchParts, filePath, oldText, newText, action) {
1373
+ const hunks = structuredPatch(filePath, filePath, oldText, newText, undefined, undefined, {
1374
+ context: 3,
1375
+ }).hunks;
1376
+ let additions = 0;
1377
+ let deletions = 0;
1125
1378
  const parts = [`diff --git a/${filePath} b/${filePath}\n`];
1126
- if (!oldText) {
1379
+ if (action === 'create') {
1127
1380
  parts.push('new file mode 100644\n');
1128
1381
  }
1129
- else if (!newText) {
1382
+ else if (action === 'delete') {
1130
1383
  parts.push('deleted file mode 100644\n');
1131
1384
  }
1132
- parts.push(`--- ${aPath}\n`);
1133
- parts.push(`+++ ${bPath}\n`);
1134
- parts.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@\n`);
1135
- for (const line of oldLines) {
1136
- parts.push(`-${line}\n`);
1385
+ parts.push(`--- ${action === 'create' ? '/dev/null' : `a/${filePath}`}\n`);
1386
+ parts.push(`+++ ${action === 'delete' ? '/dev/null' : `b/${filePath}`}\n`);
1387
+ for (const hunk of hunks) {
1388
+ parts.push(`@@ -${hunk.oldStart},${hunk.oldLines} +${hunk.newStart},${hunk.newLines} @@\n`);
1389
+ for (const line of hunk.lines) {
1390
+ parts.push(`${line}\n`);
1391
+ if (line.startsWith('+'))
1392
+ additions++;
1393
+ else if (line.startsWith('-'))
1394
+ deletions++;
1395
+ }
1137
1396
  }
1138
- for (const line of newLines) {
1139
- parts.push(`+${line}\n`);
1397
+ diffFiles.push({ path: filePath, action, additions, deletions });
1398
+ // An empty-file create/delete produces zero hunks but is still a real
1399
+ // change — emit the header-only entry so the patch lists every changed
1400
+ // path. Only a hunk-less UPDATE (identical content, e.g. mode-only noise)
1401
+ // is genuinely nothing to show.
1402
+ if (hunks.length > 0 || action !== 'update') {
1403
+ patchParts.push(parts.join(''));
1140
1404
  }
1141
- return parts.join('');
1142
- }
1143
- /** Estimate number of lines from file size (rough: ~40 bytes per line). */
1144
- function estimateLines(sizeBytes) {
1145
- return Math.max(1, Math.round(sizeBytes / 40));
1146
1405
  }
1147
1406
  // ---------------------------------------------------------------------------
1148
1407
  // File system helpers
@@ -1244,13 +1503,11 @@ async function resolveLocalWikiMount(ctx, wiki) {
1244
1503
  changesetsUrl: `${wikiBase}/changesets`,
1245
1504
  };
1246
1505
  }
1247
- async function requireLocalWikiMount(ctx, wiki, options = {}) {
1506
+ async function requireLocalWikiMount(ctx, wiki) {
1248
1507
  const mount = await resolveLocalWikiMount(ctx, wiki);
1249
1508
  if (!mount) {
1250
- throw new Error(`wiki ${wiki.slug} is not available in a local agent mount`);
1251
- }
1252
- if (options.requireWrite && 'read_write' !== 'read_write') {
1253
- throw new Error(`wiki ${wiki.slug} mount is read-only`);
1509
+ throw new Error(`wiki ${wiki.slug} has no local workspace yet run \`parall wiki sync\` to download it ` +
1510
+ '(the sync output prints the workspace path)');
1254
1511
  }
1255
1512
  return mount;
1256
1513
  }
@@ -1539,14 +1796,21 @@ function stripNodeSectionContent(section) {
1539
1796
  }
1540
1797
  function buildIndexNodes(sections) {
1541
1798
  const nodes = [];
1799
+ // File-level frontmatter folds into only the first section of each file
1800
+ // (matching the indexed search-indexer's section-0 gating) so a
1801
+ // frontmatter-only query returns one hit per document, not one per section.
1802
+ const seenFile = new Set();
1542
1803
  for (const section of sections) {
1543
- nodes.push(buildIndexedNode(section));
1804
+ const includeFrontmatter = !seenFile.has(section.path);
1805
+ seenFile.add(section.path);
1806
+ nodes.push(buildIndexedNode(section, includeFrontmatter));
1544
1807
  }
1545
1808
  return nodes;
1546
1809
  }
1547
1810
  function parseMarkdownFile(wiki, file) {
1548
1811
  const lines = file.content.split('\n');
1549
1812
  const frontMatterEnd = detectFrontMatterEnd(lines);
1813
+ const fm = parseFrontMatter(lines, frontMatterEnd);
1550
1814
  const headings = extractMarkdownHeadings(lines, frontMatterEnd);
1551
1815
  const fileTitle = defaultFileTitle(file.path);
1552
1816
  if (headings.length === 0) {
@@ -1560,7 +1824,11 @@ function parseMarkdownFile(wiki, file) {
1560
1824
  endLine: lines.length,
1561
1825
  content: lines.slice(frontMatterEnd).join('\n').trim(),
1562
1826
  });
1563
- return !docNode.content ? [] : [docNode];
1827
+ // Keep an empty-body doc node when it carries frontmatter, so a
1828
+ // frontmatter-only page still exposes its metadata — matching the Go
1829
+ // parser, which always returns the doc section for a no-heading file.
1830
+ const keep = !!docNode.content || hasFrontMatter(fm);
1831
+ return applyFrontMatter(keep ? [docNode] : [], fm);
1564
1832
  }
1565
1833
  const nodes = [];
1566
1834
  const firstHeadingLine = headings[0].line;
@@ -1597,7 +1865,24 @@ function parseMarkdownFile(wiki, file) {
1597
1865
  .trim(),
1598
1866
  }));
1599
1867
  }
1600
- return nodes;
1868
+ return applyFrontMatter(nodes, fm);
1869
+ }
1870
+ // applyFrontMatter copies file-level frontmatter onto every section parsed from
1871
+ // the file (frontmatter is a document-level property in OKF). Mirrors the Go
1872
+ // implementation in server/pkg/markdown/section.go. No-op when empty.
1873
+ function applyFrontMatter(sections, fm) {
1874
+ if (!hasFrontMatter(fm)) {
1875
+ return sections;
1876
+ }
1877
+ for (const section of sections) {
1878
+ if (fm.type !== undefined)
1879
+ section.type = fm.type;
1880
+ if (fm.description !== undefined)
1881
+ section.description = fm.description;
1882
+ if (fm.tags !== undefined)
1883
+ section.tags = fm.tags;
1884
+ }
1885
+ return sections;
1601
1886
  }
1602
1887
  function buildNodeSection(wiki, input) {
1603
1888
  const content = input.content.trim();
@@ -1617,11 +1902,14 @@ function buildNodeSection(wiki, input) {
1617
1902
  content,
1618
1903
  };
1619
1904
  }
1620
- function buildIndexedNode(section) {
1905
+ function buildIndexedNode(section, includeFrontmatter) {
1621
1906
  const searchText = [
1622
1907
  section.path,
1623
1908
  section.title,
1624
1909
  section.heading_path?.join(' ') ?? '',
1910
+ ...(includeFrontmatter
1911
+ ? [section.type ?? '', section.description ?? '', section.tags?.join(' ') ?? '']
1912
+ : []),
1625
1913
  section.content,
1626
1914
  ]
1627
1915
  .join('\n')
@@ -1965,7 +2253,8 @@ function extractMarkdownHeadings(lines, startIndex) {
1965
2253
  }
1966
2254
  return headings;
1967
2255
  }
1968
- function detectFrontMatterEnd(lines) {
2256
+ // Exported for unit testing; mirrors server/pkg/markdown/section.go.
2257
+ export function detectFrontMatterEnd(lines) {
1969
2258
  if (lines.length === 0) {
1970
2259
  return 0;
1971
2260
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/cli",
3
- "version": "1.34.0",
3
+ "version": "1.35.0",
4
4
  "description": "CLI client for Parall — universal agent & human access to Parall API",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -33,12 +33,16 @@
33
33
  "dependencies": {
34
34
  "@modelcontextprotocol/sdk": "1.27.1",
35
35
  "commander": "^13.0.0",
36
+ "diff": "^8.0.3",
37
+ "js-yaml": "^4.1.0",
36
38
  "zod": "^4.3.6",
37
- "@parall/sdk": "1.34.0"
39
+ "@parall/sdk": "1.35.0"
38
40
  },
39
41
  "devDependencies": {
42
+ "@types/js-yaml": "^4.0.9",
40
43
  "@types/node": "^22.0.0",
41
- "typescript": "^5.7.0"
44
+ "typescript": "^5.7.0",
45
+ "@parall/agent-core": "1.35.0"
42
46
  },
43
47
  "scripts": {
44
48
  "build": "tsc",