qriton 0.4.1 → 0.5.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,84 @@
1
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { isAbsolute, relative, resolve, win32 } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ const USAGE = 'Usage: node bin/release-readiness.mjs [--root <package-folder>] [--channel <name>]';
6
+ const DEFAULT_CHANNEL = 'stable'; // Reviewed example setting, not a live memory lookup.
7
+ const inside = (root, file) => {
8
+ const rel = relative(root, file);
9
+ return rel === '' || (!isAbsolute(rel) && rel !== '..' && !rel.startsWith('../') && !rel.startsWith('..\\'));
10
+ };
11
+
12
+ function regularFile(root, name) {
13
+ const label = JSON.stringify(name);
14
+ if (isAbsolute(name) || win32.isAbsolute(name) || /^[A-Za-z]:/.test(name) || /[\\\u0000-\u001f\u007f]/.test(name)) {
15
+ return { issue: `${label} must be a relative, forward-slash-separated file path.` };
16
+ }
17
+ const file = resolve(root, name);
18
+ if (!inside(root, file)) return { issue: `${label} escapes the package root.` };
19
+ try {
20
+ const actual = realpathSync(file);
21
+ if (!inside(root, actual)) return { issue: `${label} resolves outside the package root.` };
22
+ if (!statSync(actual).isFile()) return { issue: `${label} is not a regular file.` };
23
+ return { path: actual };
24
+ } catch (error) {
25
+ return { issue: ['ENOENT', 'ENOTDIR'].includes(error.code)
26
+ ? `${label} is missing.` : `${label} cannot be inspected (${error.code ?? 'filesystem error'}).` };
27
+ }
28
+ }
29
+
30
+ export function inspectRelease(root, channel = DEFAULT_CHANNEL) {
31
+ if (typeof root !== 'string' || !root.trim()) throw new Error('Invalid package root.');
32
+ if (typeof channel !== 'string' || !/^[a-z0-9][a-z0-9._-]{0,63}$/i.test(channel)) throw new Error('Invalid channel name.');
33
+ const actualRoot = realpathSync(resolve(root));
34
+ if (!statSync(actualRoot).isDirectory()) throw new Error('Package root must be a directory.');
35
+ const manifestFile = regularFile(actualRoot, 'package.json');
36
+ if (manifestFile.issue) throw new Error(`Invalid manifest: ${manifestFile.issue}`);
37
+ let manifest;
38
+ try { manifest = JSON.parse(readFileSync(manifestFile.path, 'utf8')); }
39
+ catch { throw new Error('Invalid manifest: package.json must contain readable JSON.'); }
40
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)
41
+ || !['name', 'version'].every(key => typeof manifest[key] === 'string' && manifest[key].trim())
42
+ || !Array.isArray(manifest.files) || !manifest.files.every(file => typeof file === 'string' && file.trim())) {
43
+ throw new Error('Invalid manifest: name and version must be nonempty strings; files must be an array of nonempty strings.');
44
+ }
45
+
46
+ const issues = [];
47
+ const readme = regularFile(actualRoot, 'README.md');
48
+ if (readme.issue) issues.push(readme.issue);
49
+ else {
50
+ try {
51
+ const versions = [...readFileSync(readme.path, 'utf8').matchAll(/^Version:[ \t]*([^\r\n]*)$/gm)].map(match => match[1].trim());
52
+ if (versions.length !== 1) issues.push('README.md must contain exactly one Version: line.');
53
+ else if (versions[0] !== manifest.version) {
54
+ issues.push(`README.md Version: ${JSON.stringify(versions[0])} does not match package.json version ${JSON.stringify(manifest.version)}.`);
55
+ }
56
+ } catch (error) { issues.push(`README.md cannot be read (${error.code ?? 'filesystem error'}).`); }
57
+ }
58
+ for (const file of new Set(manifest.files)) {
59
+ const checked = regularFile(actualRoot, file);
60
+ if (checked.issue && !issues.includes(checked.issue)) issues.push(checked.issue);
61
+ }
62
+ return { name: manifest.name, version: manifest.version, channel, prerelease: channel !== 'stable', ready: issues.length === 0, issues };
63
+ }
64
+
65
+ export function main(args = process.argv.slice(2)) {
66
+ let root = process.cwd();
67
+ let channel;
68
+ const seen = new Set();
69
+ for (let i = 0; i < args.length; i++) {
70
+ const option = args[i], value = args[i + 1];
71
+ if (!['--root', '--channel'].includes(option) || seen.has(option) || !value || value.startsWith('--')) throw new Error(USAGE);
72
+ seen.add(option); i++;
73
+ if (option === '--root') root = value;
74
+ else channel = value;
75
+ }
76
+ const report = inspectRelease(root, channel);
77
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
78
+ return report.ready ? 0 : 1;
79
+ }
80
+
81
+ if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
82
+ try { process.exitCode = main(); }
83
+ catch (error) { process.stderr.write(`${error.message}\n`); process.exitCode = 2; }
84
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "forgepack",
3
+ "version": "0.7.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "description": "Reviewed, offline release-readiness example for the Qriton HLM demonstration.",
7
+ "files": ["README.md", "bin/release-readiness.mjs"],
8
+ "scripts": {
9
+ "test": "node --test",
10
+ "release:check": "node bin/release-readiness.mjs"
11
+ },
12
+ "engines": { "node": ">=22.16.0" }
13
+ }
@@ -0,0 +1,119 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from 'node:fs';
3
+ import { spawnSync } from 'node:child_process';
4
+ import { tmpdir } from 'node:os';
5
+ import { join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import { test } from 'node:test';
8
+ import { inspectRelease } from '../bin/release-readiness.mjs';
9
+
10
+ const cli = fileURLToPath(new URL('../bin/release-readiness.mjs', import.meta.url));
11
+ function fixture(t, { version = '1.2.3', readme = `# Example\n\nVersion: ${version}\n`, files = ['README.md', 'dist/index.mjs'] } = {}) {
12
+ const directory = mkdtempSync(join(tmpdir(), 'forgepack-reviewed-'));
13
+ t.after(() => rmSync(directory, { recursive: true, force: true }));
14
+ const root = join(directory, 'package');
15
+ mkdirSync(join(root, 'dist'), { recursive: true });
16
+ writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'example', version, files }));
17
+ if (readme !== null) writeFileSync(join(root, 'README.md'), readme);
18
+ writeFileSync(join(root, 'dist/index.mjs'), '');
19
+ return { root, directory };
20
+ }
21
+
22
+ function invoke(root, args = []) {
23
+ const result = spawnSync(process.execPath, [cli, '--root', root, ...args], { encoding: 'utf8', timeout: 5000 });
24
+ assert.ifError(result.error); assert.equal(result.signal, null);
25
+ return result;
26
+ }
27
+
28
+ test('ready report accepts an empty regular file and preserves explicit channels', t => {
29
+ const { root } = fixture(t);
30
+ assert.deepEqual(inspectRelease(root), { name: 'example', version: '1.2.3', channel: 'stable', prerelease: false, ready: true, issues: [] });
31
+ assert.equal(inspectRelease(root, 'nightly').channel, 'nightly');
32
+ assert.equal(inspectRelease(root, 'nightly').prerelease, true);
33
+ const result = invoke(root, ['--channel', 'nightly']);
34
+ assert.equal(result.status, 0); assert.equal(JSON.parse(result.stdout).channel, 'nightly');
35
+ });
36
+
37
+ test('missing, empty, duplicate, and stale README Version lines block readiness', t => {
38
+ for (const readme of ['# Example\n', 'Version:\n', 'Version: 1.2.3\nVersion: 1.2.3\n', 'Version: 1.2.2\n']) {
39
+ const { root } = fixture(t, { readme });
40
+ const report = inspectRelease(root);
41
+ assert.equal(report.ready, false); assert.match(report.issues.join('\n'), /README.md.*Version/);
42
+ }
43
+ });
44
+
45
+ test('the complete prerelease and build version must match', t => {
46
+ const version = '1.2.3-beta.2+build.7';
47
+ assert.equal(inspectRelease(fixture(t, { version }).root).ready, true);
48
+ for (const value of ['1.2.3', '1.2.3-beta.1+build.7', '1.2.3-beta.2+build.8']) {
49
+ const { root } = fixture(t, { version, readme: `Version: ${value}\n` });
50
+ assert.equal(inspectRelease(root).ready, false);
51
+ }
52
+ });
53
+
54
+ test('missing README and declared files produce not-ready reports and exit 1', t => {
55
+ const { root } = fixture(t, { readme: null });
56
+ rmSync(join(root, 'dist/index.mjs'));
57
+ const result = invoke(root);
58
+ assert.equal(result.status, 1);
59
+ const report = JSON.parse(result.stdout);
60
+ assert.equal(report.ready, false); assert.equal(report.issues.length, 2);
61
+ assert.match(report.issues.join('\n'), /README.md/); assert.match(report.issues.join('\n'), /dist\/index.mjs/);
62
+ });
63
+
64
+ test('directories cannot stand in for declared files or README', t => {
65
+ const declared = fixture(t, { files: ['dist'] });
66
+ assert.match(inspectRelease(declared.root).issues.join('\n'), /dist.*not a regular file/);
67
+ const readme = fixture(t, { readme: null }); mkdirSync(join(readme.root, 'README.md'));
68
+ assert.match(inspectRelease(readme.root).issues.join('\n'), /README.md.*not a regular file/);
69
+ });
70
+
71
+ test('absolute paths and lexical escapes are rejected even when the file exists', t => {
72
+ const { root, directory } = fixture(t);
73
+ const outside = join(directory, 'outside.mjs'); writeFileSync(outside, '');
74
+ for (const file of [outside, '../outside.mjs', 'C:\\outside.mjs', '..\\outside.mjs']) {
75
+ writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'example', version: '1.2.3', files: [file] }));
76
+ const report = inspectRelease(root);
77
+ assert.equal(report.ready, false); assert.match(report.issues.join('\n'), /relative|escapes/);
78
+ }
79
+ });
80
+
81
+ test('file and parent-directory symlink escapes are rejected; in-root links work', t => {
82
+ const { root, directory } = fixture(t, { files: ['linked.mjs', 'linked-dir/outside.mjs'] });
83
+ writeFileSync(join(directory, 'outside.mjs'), '');
84
+ symlinkSync(join(directory, 'outside.mjs'), join(root, 'linked.mjs'));
85
+ symlinkSync(directory, join(root, 'linked-dir'), 'dir');
86
+ const report = inspectRelease(root);
87
+ assert.equal(report.ready, false); assert.equal(report.issues.length, 2);
88
+ assert.ok(report.issues.every(issue => issue.includes('outside the package root')));
89
+ rmSync(join(root, 'linked.mjs')); symlinkSync(join(root, 'dist/index.mjs'), join(root, 'linked.mjs'));
90
+ writeFileSync(join(root, 'package.json'), JSON.stringify({ name: 'example', version: '1.2.3', files: ['linked.mjs'] }));
91
+ assert.equal(inspectRelease(root).ready, true);
92
+ });
93
+
94
+ test('README symlink escapes are rejected before reading the matching version', t => {
95
+ const { root, directory } = fixture(t, { readme: null });
96
+ const outside = join(directory, 'outside.md'); writeFileSync(outside, 'Version: 1.2.3\n');
97
+ symlinkSync(outside, join(root, 'README.md'));
98
+ assert.equal(inspectRelease(root).ready, false);
99
+ assert.match(inspectRelease(root).issues.join('\n'), /README.md.*outside/);
100
+ });
101
+
102
+ test('invalid manifest JSON, shape, and outside-root manifest exit 2', t => {
103
+ const { root, directory } = fixture(t);
104
+ for (const raw of ['{', '[]', '{"name":"example","version":"1.2.3"}', '{"name":"example","version":"1.2.3","files":[42]}']) {
105
+ writeFileSync(join(root, 'package.json'), raw);
106
+ const result = invoke(root); assert.equal(result.status, 2); assert.equal(result.stdout, ''); assert.match(result.stderr, /Invalid manifest/);
107
+ }
108
+ rmSync(join(root, 'package.json'));
109
+ const outside = join(directory, 'outside.json'); writeFileSync(outside, JSON.stringify({ name: 'example', version: '1.2.3', files: [] }));
110
+ symlinkSync(outside, join(root, 'package.json'));
111
+ assert.equal(invoke(root).status, 2);
112
+ });
113
+
114
+ test('missing values, unknown or repeated flags, and invalid channels exit 2', t => {
115
+ const { root } = fixture(t);
116
+ for (const args of [['--channel'], ['--unknown', 'value'], ['--root', root], ['--channel', 'beta', '--channel', 'stable'], ['--channel', 'bad channel']]) {
117
+ const result = invoke(root, args); assert.equal(result.status, 2); assert.equal(result.stdout, ''); assert.ok(result.stderr.trim());
118
+ }
119
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qriton",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Qriton CLI is a terminal coding agent that runs on infrastructure you control. Local models or any major model API, with a memory record, HLM, and Energy Language.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,8 +8,11 @@
8
8
  "qr": "./dist/qriton.js"
9
9
  },
10
10
  "files": [
11
- "dist",
11
+ "dist/qriton.js",
12
12
  "README.md",
13
+ "docs/hlm.md",
14
+ "docs/hlm-setup.md",
15
+ "docs/workspace-trust.md",
13
16
  "SECURITY.md",
14
17
  "deploy/nats",
15
18
  "examples/energy",
@@ -44,12 +47,14 @@
44
47
  "@qriton/shield": "3.3.0",
45
48
  "ink": "7.1.1",
46
49
  "openai": "7.15.0",
50
+ "qrcode": "^1.5.4",
47
51
  "react": "19.3.0",
48
52
  "string-width": "8.2.0",
49
53
  "undici": "7.29.1"
50
54
  },
51
55
  "devDependencies": {
52
56
  "@types/node": "^24.0.0",
57
+ "@types/qrcode": "^1.5.6",
53
58
  "@types/react": "^19.2.0",
54
59
  "esbuild": "^0.28.2",
55
60
  "typescript": "^5.9.0"
@@ -1,8 +1,8 @@
1
1
  {
2
- "version": "0.4.1",
2
+ "version": "0.5.1",
3
3
  "supportedCodingModel": "local Ollama, plus xAI, Anthropic, OpenAI, and OpenAI-compatible APIs",
4
- "codingEvidence": "One session keeps its transcript, tools, and memory graph across those providers. HLM and Energy Language are separate local workbenches.",
5
- "localModels": "Optional. A bare qriton command still starts the configured local model. Local models are not the supported coding path.",
4
+ "codingEvidence": "In one repeat of 30 small held-out tasks on a 16 GB laptop, qwen3.5:4b produced 27 passing artifacts; 24 also had successful process exits. Four runs timed out, including three with passing artifacts. openai/gpt-5-mini passed two small editing fixtures. These checks do not establish representative coding quality or superiority over other tools. HLM and Energy Language are separate local workbenches.",
5
+ "localModels": "Supported through Ollama. A fresh installation defaults to qwen3.5:4b; a bare qriton command uses the configured model. Local inference requires sufficient available memory and compute. The light preset is not a memory cap or a guarantee of desktop responsiveness.",
6
6
  "executionDefault": "host",
7
7
  "hostExecution": "Approved commands run on this machine.",
8
8
  "containerImage": {