@north-light/crouter 0.3.166 → 0.3.167

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.
@@ -6,11 +6,12 @@
6
6
  // sender (`from`) is visible in both the read body and the artifact title.
7
7
  import { test, before, after, beforeEach } from 'node:test';
8
8
  import assert from 'node:assert/strict';
9
- import { mkdtempSync, rmSync } from 'node:fs';
9
+ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { join } from 'node:path';
12
12
  import { createNode } from '../canvas/canvas.js';
13
13
  import { closeDb } from '../canvas/db.js';
14
+ import { contextDir } from '../canvas/paths.js';
14
15
  import { appendInbox } from '../feed/inbox.js';
15
16
  import { buildCorpus, resolveRef } from '../canvas/history.js';
16
17
  let home;
@@ -39,6 +40,15 @@ after(() => {
39
40
  rmSync(home, { recursive: true, force: true });
40
41
  delete process.env['CRTR_HOME'];
41
42
  });
43
+ test('history corpus skips malformed frontmatter in another cwd', () => {
44
+ const broken = createNode(node('broken-frontmatter', '/tmp/hist-broken'));
45
+ const valid = createNode(node('valid-history', '/tmp/hist-valid'));
46
+ writeFileSync(join(contextDir(broken.node_id), 'copied-source.md'), '---\ndesc: "unterminated\n---\nsource text\n');
47
+ writeFileSync(join(contextDir(valid.node_id), 'result.md'), '---\ntitle: result\n---\n0a0afd613\n');
48
+ const corpus = buildCorpus({ allCwds: true }, { types: ['report', 'doc', 'roadmap'] });
49
+ assert.equal(corpus.some((a) => a.ref === `${broken.node_id}:context/copied-source.md`), false, 'invalid artifact is skipped');
50
+ assert.ok(corpus.some((a) => a.loadBody().includes('0a0afd613')), 'valid artifacts remain searchable');
51
+ });
42
52
  test('default history excludes inbox; --type inbox includes it, with full sender-attributed bodies', () => {
43
53
  const cwd = '/tmp/hist-work';
44
54
  createNode(node('sender-1', cwd));
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,46 @@
1
+ import assert from 'node:assert/strict';
2
+ import { execFileSync, spawnSync } from 'node:child_process';
3
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, readlinkSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
4
+ import { tmpdir } from 'node:os';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import test from 'node:test';
8
+ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
9
+ const SELF_UPDATE = join(ROOT, 'scripts', 'self-update.mjs');
10
+ test('self-update installs the downloaded release without replacing the stable global shim', () => {
11
+ const directory = mkdtempSync(join(tmpdir(), 'crtr-self-update-test-'));
12
+ try {
13
+ const release = join(directory, 'release');
14
+ const archive = join(directory, 'release.tgz');
15
+ const bin = join(directory, 'bin');
16
+ const stableShim = join(bin, 'crtr');
17
+ const marker = join(directory, 'selected');
18
+ const calls = join(directory, 'npm-calls');
19
+ const fakeBin = join(directory, 'fake-bin');
20
+ mkdirSync(join(release, 'scripts'), { recursive: true });
21
+ mkdirSync(bin);
22
+ mkdirSync(fakeBin);
23
+ writeFileSync(join(release, 'scripts', 'install-runtime.mjs'), `import { writeFileSync } from 'node:fs';\nwriteFileSync(process.env.CRTR_SELF_UPDATE_MARKER, 'selected');\n`);
24
+ execFileSync('tar', ['-czf', archive, '-C', release, '.']);
25
+ symlinkSync('../stable-crtr', stableShim);
26
+ writeFileSync(join(fakeBin, 'npm'), `#!/usr/bin/env node\nimport { copyFileSync, appendFileSync } from 'node:fs';\nimport { basename, join } from 'node:path';\nconst args = process.argv.slice(2);\nappendFileSync(process.env.CRTR_SELF_UPDATE_CALLS, args.join(' ') + '\\n');\nif (args[0] !== 'pack') process.exit(1);\nconst destination = args[args.indexOf('--pack-destination') + 1];\ncopyFileSync(process.env.CRTR_SELF_UPDATE_ARCHIVE, join(destination, basename(process.env.CRTR_SELF_UPDATE_ARCHIVE)));\nprocess.stdout.write(JSON.stringify([{ filename: basename(process.env.CRTR_SELF_UPDATE_ARCHIVE) }]));\n`);
27
+ chmodSync(join(fakeBin, 'npm'), 0o755);
28
+ const result = spawnSync(process.execPath, [SELF_UPDATE], {
29
+ encoding: 'utf8',
30
+ env: {
31
+ ...process.env,
32
+ PATH: `${fakeBin}:${process.env.PATH}`,
33
+ CRTR_SELF_UPDATE_ARCHIVE: archive,
34
+ CRTR_SELF_UPDATE_CALLS: calls,
35
+ CRTR_SELF_UPDATE_MARKER: marker,
36
+ },
37
+ });
38
+ assert.equal(result.status, 0, result.stderr);
39
+ assert.equal(readFileSync(marker, 'utf8'), 'selected');
40
+ assert.equal(readlinkSync(stableShim), '../stable-crtr');
41
+ assert.match(readFileSync(calls, 'utf8'), /^pack @north-light\/crouter@latest --json --pack-destination .+\/crtr-self-update-\S+\n$/);
42
+ }
43
+ finally {
44
+ rmSync(directory, { recursive: true, force: true });
45
+ }
46
+ });
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import { readConfig, readState, updateState } from './config.js';
3
3
  import { nowIso } from './fs-utils.js';
4
4
  import { info } from './output.js';
5
- import { selfCheck, contentCheck } from './self-update.js';
5
+ import { SELF_UPDATE_SCRIPT, selfCheck, contentCheck } from './self-update.js';
6
6
  const HOUR_MS = 60 * 60 * 1000;
7
7
  const SKIP_SUBCOMMANDS = new Set([
8
8
  'update',
@@ -27,7 +27,7 @@ function withinInterval(lastIso, intervalHours) {
27
27
  return Date.now() - last < intervalHours * HOUR_MS;
28
28
  }
29
29
  function spawnDetachedSelfUpdate() {
30
- const child = spawn('npm', ['i', '-g', '@north-light/crouter@latest'], {
30
+ const child = spawn(process.execPath, [SELF_UPDATE_SCRIPT], {
31
31
  detached: true,
32
32
  stdio: 'ignore',
33
33
  });
@@ -15,6 +15,7 @@ import { getNode, listNodes, view } from './canvas.js';
15
15
  import { contextDir, reportsDir, nodeDir, inboxPath } from './paths.js';
16
16
  import { readTextIfExists, walkFiles, pathExists } from '../fs-utils.js';
17
17
  import { parseFrontmatterGeneric } from '../frontmatter.js';
18
+ import { warn } from '../output.js';
18
19
  /** The cwd the caller means by default: the calling node's cwd (resolved from
19
20
  * CRTR_NODE_ID via the canvas data layer), falling back to the process cwd
20
21
  * when not run as a node. */
@@ -242,6 +243,11 @@ function listDocFiles(nodeId) {
242
243
  return [];
243
244
  }
244
245
  }
246
+ /** Keep one malformed artifact from breaking a cross-node corpus scan. */
247
+ function warnInvalidFrontmatter(path, error) {
248
+ const message = (error instanceof Error ? error.message : String(error)).split('\n')[0];
249
+ warn(`invalid frontmatter in ${path}: ${message}`);
250
+ }
245
251
  /** The node desc lives in meta.json, not the row — read it once per survivor. */
246
252
  function nodeDescOf(nodeId) {
247
253
  const n = getNode(nodeId);
@@ -281,7 +287,15 @@ export function buildCorpus(scope, corpus) {
281
287
  out.push(metaArtifact(row, desc));
282
288
  if (want.has('report')) {
283
289
  for (const f of listReportFiles(row.node_id)) {
284
- const a = reportArtifact(row, desc, f);
290
+ const abs = join(reportsDir(row.node_id), f);
291
+ let a;
292
+ try {
293
+ a = reportArtifact(row, desc, f);
294
+ }
295
+ catch (e) {
296
+ warnInvalidFrontmatter(abs, e);
297
+ continue;
298
+ }
285
299
  if (a === null)
286
300
  continue;
287
301
  if (wantReportKind !== undefined && a.reportKind !== wantReportKind)
@@ -291,7 +305,14 @@ export function buildCorpus(scope, corpus) {
291
305
  }
292
306
  if (want.has('doc') || want.has('roadmap')) {
293
307
  for (const abs of listDocFiles(row.node_id)) {
294
- const a = docArtifact(row, desc, abs);
308
+ let a;
309
+ try {
310
+ a = docArtifact(row, desc, abs);
311
+ }
312
+ catch (e) {
313
+ warnInvalidFrontmatter(abs, e);
314
+ continue;
315
+ }
295
316
  if (a.source === 'roadmap' && !want.has('roadmap'))
296
317
  continue;
297
318
  if (a.source === 'doc' && !want.has('doc'))
@@ -1,3 +1,4 @@
1
+ export declare const SELF_UPDATE_SCRIPT: string;
1
2
  export declare function currentVersion(): string;
2
3
  export declare function selfUpdate(): void;
3
4
  /** Check whether a newer crtr version is available on npm.
@@ -15,16 +15,18 @@ const __dirname = dirname(__filename);
15
15
  // src/core/self-update.ts → up to src/ → up to pkg root
16
16
  const PKG_ROOT = join(__dirname, '..', '..');
17
17
  const PACKAGE_JSON_PATH = join(PKG_ROOT, 'package.json');
18
+ export const SELF_UPDATE_SCRIPT = join(PKG_ROOT, 'scripts', 'self-update.mjs');
18
19
  export function currentVersion() {
19
20
  const raw = readFileSync(PACKAGE_JSON_PATH, 'utf8');
20
21
  const parsed = JSON.parse(raw);
21
22
  return parsed.version;
22
23
  }
23
24
  export function selfUpdate() {
24
- const res = spawnSync('npm', ['i', '-g', '@north-light/crouter@latest'], { stdio: 'inherit' });
25
- if (res.status !== 0) {
26
- throw general('npm install failed');
27
- }
25
+ const res = spawnSync(process.execPath, [SELF_UPDATE_SCRIPT], { stdio: 'inherit' });
26
+ if (res.error)
27
+ throw general(`self-update failed: ${res.error.message}`);
28
+ if (res.status !== 0)
29
+ throw general(`self-update failed (${res.status ?? res.signal ?? 'unknown'})`);
28
30
  }
29
31
  /** Check whether a newer crtr version is available on npm.
30
32
  * Warns to stderr if network unavailable; returns {current, latest} or null if unreachable. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.166",
3
+ "version": "0.3.167",
4
4
  "description": "crtr — agent runtime with memory, plugins, and marketplaces",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -70,6 +70,7 @@
70
70
  "scripts/patch-pi-tree-root-ordering.mjs",
71
71
  "scripts/install-runtime.mjs",
72
72
  "scripts/runtime-lock.mjs",
73
+ "scripts/self-update.mjs",
73
74
  "runtime.lock.json"
74
75
  ],
75
76
  "scripts": {
@@ -107,7 +108,7 @@
107
108
  },
108
109
  "license": "MIT",
109
110
  "dependencies": {
110
- "@crouton-kit/humanloop": "0.4.21",
111
+ "@crouton-kit/humanloop": "0.4.22",
111
112
  "@earendil-works/pi-agent-core": "0.83.0",
112
113
  "@earendil-works/pi-ai": "0.83.0",
113
114
  "@earendil-works/pi-coding-agent": "0.83.0",
package/runtime.lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@north-light/crouter",
3
- "version": "0.3.166",
3
+ "version": "0.3.167",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@north-light/crouter",
9
- "version": "0.3.166",
9
+ "version": "0.3.167",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT",
12
12
  "dependencies": {
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from 'node:child_process';
3
+ import { mkdirSync, mkdtempSync, rmSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+
7
+ const PACKAGE_NAME = '@north-light/crouter';
8
+
9
+ function run(command, args, options) {
10
+ const result = spawnSync(command, args, options);
11
+ if (result.error) throw new Error(`${command} ${args.join(' ')}: ${result.error.message}`);
12
+ if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} failed (${result.status ?? result.signal ?? 'unknown'})`);
13
+ return result;
14
+ }
15
+
16
+ function latestArchive(directory) {
17
+ const result = run('npm', ['pack', `${PACKAGE_NAME}@latest`, '--json', '--pack-destination', directory], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'inherit'] });
18
+ const packed = JSON.parse(result.stdout);
19
+ if (!Array.isArray(packed) || typeof packed[0]?.filename !== 'string') throw new Error('npm pack did not report an archive');
20
+ return join(directory, packed[0].filename);
21
+ }
22
+
23
+ const temporary = mkdtempSync(join(tmpdir(), 'crtr-self-update-'));
24
+ try {
25
+ const archive = latestArchive(temporary);
26
+ const release = join(temporary, 'package');
27
+ mkdirSync(release);
28
+ run('tar', ['-xzf', archive, '--strip-components=1', '-C', release]);
29
+ run(process.execPath, [join(release, 'scripts', 'install-runtime.mjs'), release], { stdio: 'inherit' });
30
+ } finally {
31
+ rmSync(temporary, { recursive: true, force: true });
32
+ }