knosky 0.6.3 → 0.7.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.
- package/CHANGELOG.md +149 -93
- package/CREDITS.md +14 -14
- package/LICENSE.md +76 -76
- package/LIMITATIONS.md +33 -23
- package/PRIVACY.md +30 -30
- package/README.md +170 -117
- package/SECURITY.md +78 -46
- package/action/post-comment.mjs +94 -89
- package/action.yml +62 -62
- package/bin/knosky.mjs +279 -105
- package/core/CONTRACT.md +70 -70
- package/core/append-only-checkpoint.mjs +215 -0
- package/core/audit-writer.mjs +317 -0
- package/core/benchmark-results.mjs +225 -225
- package/core/bundle.mjs +178 -178
- package/core/churn.mjs +23 -23
- package/core/ci.mjs +268 -268
- package/core/comparison.mjs +189 -189
- package/core/config.mjs +189 -189
- package/core/constants.mjs +13 -13
- package/core/contract.mjs +123 -123
- package/core/cross-repo.mjs +111 -111
- package/core/decision-codes.mjs +92 -0
- package/core/destination.mjs +161 -161
- package/core/district-classification.mjs +111 -0
- package/core/doctor-scorecard.mjs +369 -0
- package/core/domain-store.mjs +347 -0
- package/core/edges.mjs +43 -43
- package/core/escalate.mjs +68 -68
- package/core/freshness.mjs +198 -194
- package/core/fs-indexer.mjs +218 -218
- package/core/key-store.mjs +348 -348
- package/core/layout.mjs +46 -46
- package/core/ledger.mjs +176 -141
- package/core/local-ipc-identity.mjs +500 -0
- package/core/lod.mjs +155 -155
- package/core/mode-b.mjs +410 -0
- package/core/multi-model-benchmark.mjs +405 -405
- package/core/net-lockdown.mjs +421 -0
- package/core/onboarding.mjs +223 -223
- package/core/operator-auth.mjs +317 -0
- package/core/overlays.mjs +45 -45
- package/core/policy-lattice.mjs +142 -0
- package/core/pr-comment.mjs +198 -198
- package/core/protocol-spec.mjs +460 -460
- package/core/provenance.mjs +320 -0
- package/core/retrieve.mjs +63 -63
- package/core/route.mjs +304 -304
- package/core/schema.mjs +275 -275
- package/core/signing-tiers.mjs +1265 -0
- package/core/swarm-bench.mjs +106 -0
- package/core/swarm-coordinator.mjs +867 -0
- package/core/trust-root-rekey.mjs +410 -0
- package/mcp/server.mjs +264 -108
- package/package.json +56 -46
- package/renderer/art/kenney/buildingTiles_sheet.xml +130 -130
- package/renderer/art/kenney/cityDetails_sheet.xml +12 -12
- package/renderer/art/kenney/landscapeTiles_sheet.xml +129 -129
- package/renderer/art/kenney/sheet_allCars.xml +545 -545
- package/renderer/build-rich.mjs +43 -43
- package/renderer/city.template.html +808 -808
- package/ssot/decision-codes.json +133 -0
- package/ssot/ladder-l0-l3.md +232 -0
- package/ssot/tool-menu.json +130 -0
package/action.yml
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
name: 'KnoSky PR-GPS'
|
|
2
|
-
description: >-
|
|
3
|
-
Advisory, metadata-only navigation report posted on pull requests. Reads
|
|
4
|
-
file/folder/import structure only (never uploads code bodies), suggests
|
|
5
|
-
where to start reviewing a PR, and flags related tests/docs. Never blocks
|
|
6
|
-
or gates the build.
|
|
7
|
-
author: 'SathiaAI'
|
|
8
|
-
branding:
|
|
9
|
-
icon: 'compass'
|
|
10
|
-
color: 'blue'
|
|
11
|
-
|
|
12
|
-
inputs:
|
|
13
|
-
base:
|
|
14
|
-
description: 'Base commit SHA to diff from (e.g. github.event.pull_request.base.sha).'
|
|
15
|
-
required: true
|
|
16
|
-
head:
|
|
17
|
-
description: 'Head commit SHA to diff to (e.g. github.event.pull_request.head.sha).'
|
|
18
|
-
required: true
|
|
19
|
-
root:
|
|
20
|
-
description: 'Path to the checked-out repo to index.'
|
|
21
|
-
required: false
|
|
22
|
-
default: '.'
|
|
23
|
-
github-token:
|
|
24
|
-
description: 'Token used to post/update the PR-GPS comment. Defaults to the workflow token.'
|
|
25
|
-
required: false
|
|
26
|
-
default: ${{ github.token }}
|
|
27
|
-
fail-on-secret:
|
|
28
|
-
description: >-
|
|
29
|
-
If "true", fail this step when a secret-like pattern is found in the
|
|
30
|
-
emitted advisory artifacts themselves (never in your source tree).
|
|
31
|
-
Defaults to "false" — this action is advisory-only by design.
|
|
32
|
-
required: false
|
|
33
|
-
default: 'false'
|
|
34
|
-
|
|
35
|
-
runs:
|
|
36
|
-
using: 'composite'
|
|
37
|
-
steps:
|
|
38
|
-
- name: Set up Node for the KnoSky action itself
|
|
39
|
-
uses: actions/setup-node@v4
|
|
40
|
-
with:
|
|
41
|
-
node-version: '20'
|
|
42
|
-
|
|
43
|
-
- name: Build the KnoSky index (best-effort — never fails the build)
|
|
44
|
-
shell: bash
|
|
45
|
-
run: |
|
|
46
|
-
node "${{ github.action_path }}/core/fs-indexer.mjs" \
|
|
47
|
-
--root "${{ inputs.root }}" \
|
|
48
|
-
--out "${{ inputs.root }}/.knosky-ci-city.json" \
|
|
49
|
-
--share-safe || true
|
|
50
|
-
|
|
51
|
-
- name: Run KnoSky PR-GPS and post/update the PR comment
|
|
52
|
-
shell: bash
|
|
53
|
-
env:
|
|
54
|
-
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
55
|
-
KC_REPO: ${{ github.repository }}
|
|
56
|
-
KC_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
57
|
-
KC_BASE: ${{ inputs.base }}
|
|
58
|
-
KC_HEAD: ${{ inputs.head }}
|
|
59
|
-
KC_ROOT: ${{ inputs.root }}
|
|
60
|
-
KC_CITY: ${{ inputs.root }}/.knosky-ci-city.json
|
|
61
|
-
KC_FAIL_ON_SECRET: ${{ inputs.fail-on-secret }}
|
|
62
|
-
run: node "${{ github.action_path }}/action/post-comment.mjs"
|
|
1
|
+
name: 'KnoSky PR-GPS'
|
|
2
|
+
description: >-
|
|
3
|
+
Advisory, metadata-only navigation report posted on pull requests. Reads
|
|
4
|
+
file/folder/import structure only (never uploads code bodies), suggests
|
|
5
|
+
where to start reviewing a PR, and flags related tests/docs. Never blocks
|
|
6
|
+
or gates the build.
|
|
7
|
+
author: 'SathiaAI'
|
|
8
|
+
branding:
|
|
9
|
+
icon: 'compass'
|
|
10
|
+
color: 'blue'
|
|
11
|
+
|
|
12
|
+
inputs:
|
|
13
|
+
base:
|
|
14
|
+
description: 'Base commit SHA to diff from (e.g. github.event.pull_request.base.sha).'
|
|
15
|
+
required: true
|
|
16
|
+
head:
|
|
17
|
+
description: 'Head commit SHA to diff to (e.g. github.event.pull_request.head.sha).'
|
|
18
|
+
required: true
|
|
19
|
+
root:
|
|
20
|
+
description: 'Path to the checked-out repo to index.'
|
|
21
|
+
required: false
|
|
22
|
+
default: '.'
|
|
23
|
+
github-token:
|
|
24
|
+
description: 'Token used to post/update the PR-GPS comment. Defaults to the workflow token.'
|
|
25
|
+
required: false
|
|
26
|
+
default: ${{ github.token }}
|
|
27
|
+
fail-on-secret:
|
|
28
|
+
description: >-
|
|
29
|
+
If "true", fail this step when a secret-like pattern is found in the
|
|
30
|
+
emitted advisory artifacts themselves (never in your source tree).
|
|
31
|
+
Defaults to "false" — this action is advisory-only by design.
|
|
32
|
+
required: false
|
|
33
|
+
default: 'false'
|
|
34
|
+
|
|
35
|
+
runs:
|
|
36
|
+
using: 'composite'
|
|
37
|
+
steps:
|
|
38
|
+
- name: Set up Node for the KnoSky action itself
|
|
39
|
+
uses: actions/setup-node@v4
|
|
40
|
+
with:
|
|
41
|
+
node-version: '20'
|
|
42
|
+
|
|
43
|
+
- name: Build the KnoSky index (best-effort — never fails the build)
|
|
44
|
+
shell: bash
|
|
45
|
+
run: |
|
|
46
|
+
node "${{ github.action_path }}/core/fs-indexer.mjs" \
|
|
47
|
+
--root "${{ inputs.root }}" \
|
|
48
|
+
--out "${{ inputs.root }}/.knosky-ci-city.json" \
|
|
49
|
+
--share-safe || true
|
|
50
|
+
|
|
51
|
+
- name: Run KnoSky PR-GPS and post/update the PR comment
|
|
52
|
+
shell: bash
|
|
53
|
+
env:
|
|
54
|
+
GITHUB_TOKEN: ${{ inputs.github-token }}
|
|
55
|
+
KC_REPO: ${{ github.repository }}
|
|
56
|
+
KC_PR_NUMBER: ${{ github.event.pull_request.number }}
|
|
57
|
+
KC_BASE: ${{ inputs.base }}
|
|
58
|
+
KC_HEAD: ${{ inputs.head }}
|
|
59
|
+
KC_ROOT: ${{ inputs.root }}
|
|
60
|
+
KC_CITY: ${{ inputs.root }}/.knosky-ci-city.json
|
|
61
|
+
KC_FAIL_ON_SECRET: ${{ inputs.fail-on-secret }}
|
|
62
|
+
run: node "${{ github.action_path }}/action/post-comment.mjs"
|
package/bin/knosky.mjs
CHANGED
|
@@ -1,106 +1,280 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// KnoSky one-command launcher: index a folder -> build the city -> open it ->
|
|
3
|
-
// print the MCP config + first prompts -> start the local MCP server.
|
|
4
|
-
// Reuses the verified core/renderer/mcp scripts as child processes (no new logic to trust).
|
|
5
|
-
//
|
|
6
|
-
// npx knosky [path] # default: current folder
|
|
7
|
-
// npx knosky . --no-open # don't auto-open the browser
|
|
8
|
-
// npx knosky . --no-serve # build + print config, don't start the MCP server
|
|
9
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import fs from 'node:fs';
|
|
12
|
-
import { fileURLToPath } from 'node:url';
|
|
13
|
-
|
|
14
|
-
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
-
const ROOT = path.resolve(HERE, '..');
|
|
16
|
-
const argv = process.argv.slice(2);
|
|
17
|
-
const flags = new Set(argv.filter(a => a.startsWith('--')));
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
// ---------------------------------------------------------------------------
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
console.
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
console.log(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// KnoSky one-command launcher: index a folder -> build the city -> open it ->
|
|
3
|
+
// print the MCP config + first prompts -> start the local MCP server.
|
|
4
|
+
// Reuses the verified core/renderer/mcp scripts as child processes (no new logic to trust).
|
|
5
|
+
//
|
|
6
|
+
// npx knosky [path] # default: current folder
|
|
7
|
+
// npx knosky . --no-open # don't auto-open the browser
|
|
8
|
+
// npx knosky . --no-serve # build + print config, don't start the MCP server
|
|
9
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
|
|
14
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const ROOT = path.resolve(HERE, '..');
|
|
16
|
+
const argv = process.argv.slice(2);
|
|
17
|
+
const flags = new Set(argv.filter(a => a.startsWith('--')));
|
|
18
|
+
const NODE = process.execPath;
|
|
19
|
+
const subcommand = argv.find(a => !a.startsWith('--'));
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// doctor subcommand: surface security/sandbox status for operator review (F0.5)
|
|
23
|
+
// Dispatched BEFORE `target` is resolved (PR #60 Architect finding): `target`
|
|
24
|
+
// used to be computed unconditionally first, so `knosky doctor` would resolve
|
|
25
|
+
// 'doctor' as a filesystem path -- harmless only because this branch exits
|
|
26
|
+
// before `target` is ever read. Reordered so that stays true by construction.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
if (subcommand === 'agent-register') {
|
|
29
|
+
// Mode B: register local agent + mint lease in .knosky domain.
|
|
30
|
+
// Elevated classes require TWO operator tokens (quorum).
|
|
31
|
+
// Bootstrap: --bootstrap-operator prints operator-a only; operator-b to 0600 file.
|
|
32
|
+
const { loadDomain, registerAgentWithLease, resolveDomainRoot } = await import('../core/domain-store.mjs');
|
|
33
|
+
const { bootstrapOperator } = await import('../core/operator-auth.mjs');
|
|
34
|
+
const getArgVal = (name) => {
|
|
35
|
+
const prefix = name + '=';
|
|
36
|
+
const eq = argv.find(a => a.startsWith(prefix));
|
|
37
|
+
if (eq !== undefined) return eq.slice(prefix.length);
|
|
38
|
+
const idx = argv.indexOf(name);
|
|
39
|
+
if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) return argv[idx + 1];
|
|
40
|
+
return undefined;
|
|
41
|
+
};
|
|
42
|
+
const domainRoot = resolveDomainRoot(undefined, getArgVal('--domain'));
|
|
43
|
+
if (flags.has('--bootstrap-operator')) {
|
|
44
|
+
if (flags.has('--allow-single-operator')) {
|
|
45
|
+
console.error(JSON.stringify({
|
|
46
|
+
ok: false,
|
|
47
|
+
reason: 'allow_single_operator_disabled_for_elevated_guarantee',
|
|
48
|
+
next_action: 'Use dual bootstrap (default). Single-op cannot elevate classes; dual is required for Rule-3 quorum. If you truly need a single identity for public/internal only domain explore later via addOperator.',
|
|
49
|
+
}, null, 2));
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
const boot = bootstrapOperator(domainRoot, {
|
|
53
|
+
operatorId: getArgVal('--operator-id') || 'operator-a',
|
|
54
|
+
operatorId2: getArgVal('--operator-id-2') || 'operator-b',
|
|
55
|
+
});
|
|
56
|
+
// Dual separation: only operator-a raw token on stdout.
|
|
57
|
+
// operator-b is file-only (tokenFileB path + fingerprint in response — not its secret).
|
|
58
|
+
console.log(JSON.stringify({
|
|
59
|
+
domain: domainRoot,
|
|
60
|
+
ok: boot.ok,
|
|
61
|
+
mode: boot.mode,
|
|
62
|
+
reason: boot.reason,
|
|
63
|
+
operatorId: boot.operatorId,
|
|
64
|
+
operatorToken: boot.operatorToken,
|
|
65
|
+
fingerprint: boot.fingerprint,
|
|
66
|
+
operatorId2: boot.operatorId2,
|
|
67
|
+
fingerprint2: boot.fingerprint2,
|
|
68
|
+
tokenFileB: boot.tokenFileB,
|
|
69
|
+
tokenB_delivery: boot.tokenB_delivery,
|
|
70
|
+
warning: boot.warning,
|
|
71
|
+
}, null, 2));
|
|
72
|
+
process.exit(boot.ok ? 0 : 1);
|
|
73
|
+
}
|
|
74
|
+
const agentId = getArgVal('--agent') || getArgVal('--id') || 'local-agent';
|
|
75
|
+
const domain = loadDomain(domainRoot);
|
|
76
|
+
const classes = (getArgVal('--classes') || 'public,internal').split(',').map(s => s.trim()).filter(Boolean);
|
|
77
|
+
// Flag names are plain CLI text. Token values come only from argv/env (never hardcoded).
|
|
78
|
+
const operatorToken = getArgVal('--operator-token') || process.env.KC_OPERATOR_TOKEN;
|
|
79
|
+
const operatorToken2 = getArgVal('--operator-token-2') || process.env.KC_OPERATOR_TOKEN_2;
|
|
80
|
+
const out = registerAgentWithLease(
|
|
81
|
+
domain,
|
|
82
|
+
{ agentId, classes, role: getArgVal('--role') || 'coder' },
|
|
83
|
+
{ operatorToken, operatorToken2 },
|
|
84
|
+
);
|
|
85
|
+
if (!out.ok) {
|
|
86
|
+
console.error(JSON.stringify({ domain: domainRoot, ...out }, null, 2));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
console.log(JSON.stringify({
|
|
90
|
+
ok: true,
|
|
91
|
+
domain: domainRoot,
|
|
92
|
+
agentId: out.agentId,
|
|
93
|
+
leaseId: out.leaseId,
|
|
94
|
+
hint: 'Pass leaseId to kc_route / kc_policy_check / kc_bundle (Mode B). Elevated classes need --operator-token AND --operator-token-2 (quorum).',
|
|
95
|
+
}, null, 2));
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (subcommand === 'doctor') {
|
|
100
|
+
const { doctorScorecardLines, buildDoctorScorecard, doctorExitCode } = await import('../core/doctor-scorecard.mjs');
|
|
101
|
+
const getArgVal = (name) => {
|
|
102
|
+
const prefix = name + '=';
|
|
103
|
+
const eq = argv.find(a => a.startsWith(prefix));
|
|
104
|
+
if (eq !== undefined) return eq.slice(prefix.length);
|
|
105
|
+
const idx = argv.indexOf(name);
|
|
106
|
+
if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) return argv[idx + 1];
|
|
107
|
+
return undefined;
|
|
108
|
+
};
|
|
109
|
+
const domain = getArgVal('--domain');
|
|
110
|
+
const city = getArgVal('--city');
|
|
111
|
+
const json = flags.has('--json');
|
|
112
|
+
const card = buildDoctorScorecard({ domainRoot: domain, cityPath: city });
|
|
113
|
+
if (json) {
|
|
114
|
+
console.log(JSON.stringify(card, null, 2));
|
|
115
|
+
} else {
|
|
116
|
+
console.log('');
|
|
117
|
+
for (const line of doctorScorecardLines({ domainRoot: domain, cityPath: city })) console.log(line);
|
|
118
|
+
console.log('');
|
|
119
|
+
}
|
|
120
|
+
process.exit(doctorExitCode(card));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// swarm subcommand: L3 operator heatmap / status (DEC-113 thin floor)
|
|
125
|
+
// knosky swarm status [--domain <path>]
|
|
126
|
+
// knosky swarm bench [--domain <path>]
|
|
127
|
+
// ---------------------------------------------------------------------------
|
|
128
|
+
if (subcommand === 'swarm') {
|
|
129
|
+
const { readSwarmHeatmap, createSwarmCoordinator } = await import('../core/swarm-coordinator.mjs');
|
|
130
|
+
const { resolveDomainRoot } = await import('../core/domain-store.mjs');
|
|
131
|
+
const getArgVal = (name) => {
|
|
132
|
+
const prefix = name + '=';
|
|
133
|
+
const eq = argv.find(a => a.startsWith(prefix));
|
|
134
|
+
if (eq !== undefined) return eq.slice(prefix.length);
|
|
135
|
+
const idx = argv.indexOf(name);
|
|
136
|
+
if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) return argv[idx + 1];
|
|
137
|
+
return undefined;
|
|
138
|
+
};
|
|
139
|
+
const action = argv.find((a, i) => i > 0 && !a.startsWith('--') && a !== 'swarm') || 'status';
|
|
140
|
+
const domainRoot = resolveDomainRoot(getArgVal('--city'), getArgVal('--domain'));
|
|
141
|
+
|
|
142
|
+
if (action === 'status') {
|
|
143
|
+
let out = readSwarmHeatmap(domainRoot);
|
|
144
|
+
if (!out.ok) {
|
|
145
|
+
// Build a live snapshot so status is useful before any swarm traffic.
|
|
146
|
+
const coord = createSwarmCoordinator({ domainRoot });
|
|
147
|
+
const wr = coord.writeHeatmap();
|
|
148
|
+
out = { ok: true, path: wr.path, domainRoot, heatmap: wr.snapshot, note: 'fresh_snapshot' };
|
|
149
|
+
}
|
|
150
|
+
console.log(JSON.stringify(out, null, 2));
|
|
151
|
+
process.exit(out.ok ? 0 : 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (action === 'bench') {
|
|
155
|
+
const { runSwarmBench } = await import('../core/swarm-bench.mjs');
|
|
156
|
+
const out = runSwarmBench({ domainRoot });
|
|
157
|
+
console.log(JSON.stringify(out, null, 2));
|
|
158
|
+
process.exit(out.ok ? 0 : 1);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.error('KnoSky swarm: unknown action "' + action + '". Try: knosky swarm status | knosky swarm bench');
|
|
162
|
+
process.exit(2);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ---------------------------------------------------------------------------
|
|
166
|
+
// ci subcommand: generate PR-GPS advisory artifacts (advisory, never breaks builds)
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
if (subcommand === 'ci') {
|
|
169
|
+
const { knoskyCi } = await import('../core/ci.mjs');
|
|
170
|
+
|
|
171
|
+
// Resolve a named flag's value from argv. Supports --flag=value and --flag value.
|
|
172
|
+
const getArgVal = (name) => {
|
|
173
|
+
const prefix = name + '=';
|
|
174
|
+
const eq = argv.find(a => a.startsWith(prefix));
|
|
175
|
+
if (eq !== undefined) return eq.slice(prefix.length);
|
|
176
|
+
const idx = argv.indexOf(name);
|
|
177
|
+
if (idx !== -1 && idx + 1 < argv.length && !argv[idx + 1].startsWith('--')) {
|
|
178
|
+
return argv[idx + 1];
|
|
179
|
+
}
|
|
180
|
+
return undefined;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const ciBase = getArgVal('--base');
|
|
184
|
+
const ciHead = getArgVal('--head');
|
|
185
|
+
const ciCity = getArgVal('--city');
|
|
186
|
+
const ciFailOnSecret = flags.has('--fail-on-secret');
|
|
187
|
+
|
|
188
|
+
const { exitCode, summaryMd, routeJson, safetyJson } = await knoskyCi({
|
|
189
|
+
root: process.cwd(),
|
|
190
|
+
base: ciBase,
|
|
191
|
+
head: ciHead,
|
|
192
|
+
cityPath: ciCity,
|
|
193
|
+
failOnSecret: ciFailOnSecret,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
fs.writeFileSync('knosky-pr-summary.md', summaryMd, 'utf8');
|
|
197
|
+
fs.writeFileSync('knosky-pr-route.json', JSON.stringify(routeJson, null, 2) + '\n', 'utf8');
|
|
198
|
+
fs.writeFileSync('knosky-safety-report.json', JSON.stringify(safetyJson, null, 2) + '\n', 'utf8');
|
|
199
|
+
|
|
200
|
+
console.log(summaryMd);
|
|
201
|
+
process.exit(exitCode);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Main path (index -> build -> open -> serve): resolve the target folder only
|
|
205
|
+
// here, after both early-exit subcommands, so a subcommand name is never
|
|
206
|
+
// mistaken for a path.
|
|
207
|
+
const target = path.resolve(subcommand || '.');
|
|
208
|
+
|
|
209
|
+
if (!fs.existsSync(target)) { console.error('KnoSky: path not found: ' + target); process.exit(1); }
|
|
210
|
+
|
|
211
|
+
const outDir = path.join(target, '.knosky');
|
|
212
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
213
|
+
const cityJson = path.join(outDir, 'city-data.json');
|
|
214
|
+
const cityHtml = path.join(outDir, 'city.html');
|
|
215
|
+
|
|
216
|
+
const run = (script, args) => spawnSync(NODE, [path.join(ROOT, script), ...args], { stdio: 'inherit' });
|
|
217
|
+
|
|
218
|
+
console.log('\nKnoSky -> indexing ' + target);
|
|
219
|
+
let r = run('core/fs-indexer.mjs', ['--root', target, '--out', cityJson, '--share-safe']);
|
|
220
|
+
if (r.status !== 0) { console.error('\nKnoSky: indexing was blocked or failed (see above). Nothing was opened.'); process.exit(r.status || 1); }
|
|
221
|
+
|
|
222
|
+
r = run('renderer/build-rich.mjs', [cityJson, cityHtml]);
|
|
223
|
+
if (r.status !== 0) { console.error('\nKnoSky: building the city failed.'); process.exit(1); }
|
|
224
|
+
|
|
225
|
+
if (!flags.has('--no-open')) {
|
|
226
|
+
const isWin = process.platform === 'win32', isMac = process.platform === 'darwin';
|
|
227
|
+
const cmd = isWin ? 'cmd' : isMac ? 'open' : 'xdg-open';
|
|
228
|
+
const a = isWin ? ['/c', 'start', '', cityHtml] : [cityHtml];
|
|
229
|
+
try { spawn(cmd, a, { detached: true, stdio: 'ignore' }).unref(); } catch (_) {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const mcpServer = path.join(ROOT, 'mcp', 'server.mjs');
|
|
233
|
+
const cfg = JSON.stringify({ mcpServers: { knosky: { command: 'node', args: [mcpServer], env: { KC_CITY: cityJson } } } }, null, 2);
|
|
234
|
+
|
|
235
|
+
console.log('\n City: ' + cityHtml);
|
|
236
|
+
console.log('\nConnect your AI assistant so it answers from THIS repo, with citations.');
|
|
237
|
+
console.log('Claude Code:');
|
|
238
|
+
console.log(' claude mcp add knosky -e KC_CITY="' + cityJson + '" -- node "' + mcpServer + '"');
|
|
239
|
+
console.log('\nClaude Desktop / Cursor / VS Code MCP config:');
|
|
240
|
+
console.log(cfg.split('\n').map(l => ' ' + l).join('\n'));
|
|
241
|
+
console.log('\nThen try one of these to see the loop (grounded, cited answers):');
|
|
242
|
+
console.log(' - "Using KnoSky, where does authentication live in this repo?"');
|
|
243
|
+
console.log(' - "Using KnoSky, what are the entry points of this project?"');
|
|
244
|
+
console.log(' - "Using KnoSky, which files should I read to understand billing?"');
|
|
245
|
+
|
|
246
|
+
if (flags.has('--no-serve')) {
|
|
247
|
+
console.log('\n(--no-serve) Not starting the MCP server. Run it later with:');
|
|
248
|
+
console.log(' KC_CITY="' + cityJson + '" node "' + mcpServer + '"\n');
|
|
249
|
+
process.exit(0);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
console.log('\nStarting the local MCP server (Ctrl+C to stop)...\n');
|
|
253
|
+
// F0.5: place the evaluator-facing MCP server inside the OS-level network
|
|
254
|
+
// lockdown when the platform supports it (core/net-lockdown.mjs).
|
|
255
|
+
// PR #60 Architect finding: this import must not be allowed to crash the CLI
|
|
256
|
+
// (or worse, silently degrade) if net-lockdown.mjs is ever missing/broken --
|
|
257
|
+
// explicitly caught, always warned, same fallback path as "tool not found".
|
|
258
|
+
let _lockdownPrefix = [];
|
|
259
|
+
try {
|
|
260
|
+
const { wrapArgsForLockdown } = await import('../core/net-lockdown.mjs');
|
|
261
|
+
_lockdownPrefix = wrapArgsForLockdown();
|
|
262
|
+
} catch (err) {
|
|
263
|
+
console.error(
|
|
264
|
+
'KnoSky: could not load the network lockdown module (' + (err?.message || err) + ') '
|
|
265
|
+
+ '-- MCP server will run WITHOUT the network lockdown.'
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
if (_lockdownPrefix.length === 0) {
|
|
269
|
+
// PR #60 QA finding: don't silently run unwrapped -- tell the operator.
|
|
270
|
+
console.error(
|
|
271
|
+
'KnoSky: no OS-level network sandbox available on this platform/environment '
|
|
272
|
+
+ '(sandbox-exec/unshare not found) -- MCP server will run WITHOUT the network '
|
|
273
|
+
+ 'lockdown. Run `knosky doctor` for details.'
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
const mcp = _lockdownPrefix.length
|
|
277
|
+
? spawn(_lockdownPrefix[0], [..._lockdownPrefix.slice(1), NODE, mcpServer, cityJson], { stdio: 'inherit' })
|
|
278
|
+
: spawn(NODE, [mcpServer, cityJson], { stdio: 'inherit' });
|
|
279
|
+
mcp.on('exit', c => process.exit(c || 0));
|
|
106
280
|
process.on('SIGINT', () => { try { mcp.kill(); } catch (_) {} process.exit(0); });
|