@n8r/surface 0.0.0-stage → 0.1.0-experimental.2
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/LICENSE +202 -0
- package/LICENSE-DOCS +395 -0
- package/LICENSING.md +36 -0
- package/README.md +181 -2
- package/dist/activity.d.ts +16 -0
- package/dist/activity.js +44 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +260 -0
- package/dist/codec.d.ts +4 -0
- package/dist/codec.js +41 -0
- package/dist/codex.d.ts +72 -0
- package/dist/codex.js +510 -0
- package/dist/contracts.d.ts +129 -0
- package/dist/contracts.js +1 -0
- package/dist/html.d.ts +5 -0
- package/dist/html.js +50 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/notes.d.ts +52 -0
- package/dist/notes.js +184 -0
- package/dist/portable.d.ts +10 -0
- package/dist/portable.js +71 -0
- package/dist/revisions.d.ts +2 -0
- package/dist/revisions.js +18 -0
- package/dist/settings.d.ts +23 -0
- package/dist/settings.js +100 -0
- package/dist/setup.d.ts +9 -0
- package/dist/setup.js +161 -0
- package/dist/source-tools.d.ts +102 -0
- package/dist/source-tools.js +201 -0
- package/dist/startup.d.ts +7 -0
- package/dist/startup.js +74 -0
- package/dist/surface-draft.d.ts +19 -0
- package/dist/surface-draft.js +95 -0
- package/dist/surface-instructions.d.ts +2 -0
- package/dist/surface-instructions.js +23 -0
- package/dist/surface-store.d.ts +36 -0
- package/dist/surface-store.js +181 -0
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +184 -0
- package/docs/n8r-surface/README.md +133 -0
- package/docs/n8r-surface/codex-research-transcript.md +51 -0
- package/docs/n8r-surface/codex-research-verification.md +176 -0
- package/docs/n8r-surface/command-outcomes.md +564 -0
- package/docs/n8r-surface/contracts.md +210 -0
- package/docs/n8r-surface/critical-path.md +163 -0
- package/docs/n8r-surface/design.md +289 -0
- package/docs/n8r-surface/getting-started.md +107 -0
- package/docs/n8r-surface/host-contract.md +176 -0
- package/docs/n8r-surface/implementation-decisions.md +96 -0
- package/docs/n8r-surface/npm-distribution.md +137 -0
- package/docs/n8r-surface/npm-release.md +188 -0
- package/docs/n8r-surface/recreation-handoff.md +117 -0
- package/docs/n8r-surface/researcher-discovery.md +157 -0
- package/docs/n8r-surface/researcher-prepared-check.md +117 -0
- package/docs/n8r-surface/researcher-reconciliation-trial.md +149 -0
- package/docs/n8r-surface/researcher-repository-check.md +95 -0
- package/docs/n8r-surface/researcher-skeleton-check.md +84 -0
- package/docs/n8r-surface/researcher-skill-verification.md +725 -0
- package/docs/n8r-surface/researcher-skill.md +255 -0
- package/docs/n8r-surface/researcher-vault.md +134 -0
- package/docs/n8r-surface/save-reopen-transcript.md +211 -0
- package/docs/n8r-surface/save-reopen-verification.md +152 -0
- package/docs/n8r-surface/startup-human-check.md +172 -0
- package/docs/n8r-surface/startup-verification.md +161 -0
- package/docs/n8r-surface/startup.md +151 -0
- package/docs/n8r-surface/surface-revisions-transcript.md +552 -0
- package/docs/n8r-surface/surface-revisions-verification.md +161 -0
- package/docs/n8r-surface/vault-setup-verification.md +210 -0
- package/docs/n8r-surface/verification.md +117 -0
- package/docs/n8r-surface/versioning.md +183 -0
- package/examples/researcher/README.md +92 -0
- package/examples/researcher/composed.surface.json +430 -0
- package/examples/researcher/composed.surface.yaml +283 -0
- package/examples/researcher/harbor-r1.research.html +193 -0
- package/examples/researcher/harbor-r2.research.html +226 -0
- package/examples/researcher/lumen-manual-edit.pending.html +271 -0
- package/examples/researcher/lumen-r1.research.html +271 -0
- package/examples/researcher/lumen-r2.research.html +280 -0
- package/examples/researcher/sources/harbor-api.md +16 -0
- package/examples/researcher/sources/harbor-auth.md +8 -0
- package/examples/researcher/sources/lumen-guide.md +28 -0
- package/examples/researcher/sources/lumen-settings.md +9 -0
- package/package.json +45 -4
- package/plugins/n8r-surface/.codex-plugin/plugin.json +31 -0
- package/plugins/n8r-surface/LICENSE +202 -0
- package/plugins/n8r-surface/LICENSE-DOCS +395 -0
- package/plugins/n8r-surface/LICENSING.md +36 -0
- package/plugins/n8r-surface/assets/icon.svg +5 -0
- package/plugins/n8r-surface/plugin.json +27 -0
- package/plugins/n8r-surface/skills/research/SKILL.md +122 -0
- package/plugins/n8r-surface/skills/research/agents/openai.yaml +4 -0
- package/plugins/n8r-surface/skills/research/references/discovery-map.md +160 -0
- package/plugins/n8r-surface/skills/research/references/model-meanings.md +55 -0
- package/plugins/n8r-surface/skills/research/references/skeleton-surface.md +105 -0
- package/plugins/n8r-surface/skills/research/references/working-notes.md +116 -0
package/README.md
CHANGED
|
@@ -1,3 +1,182 @@
|
|
|
1
|
-
#
|
|
1
|
+
# n8r-surface
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The home of the **AI Surface Model**, an open model for describing the connected
|
|
4
|
+
components, configuration and relationships of an AI system, at the level of
|
|
5
|
+
detail available or useful. This repository also develops the experimental
|
|
6
|
+
`n8r-surface` command and Researcher library.
|
|
7
|
+
|
|
8
|
+
> [!NOTE]
|
|
9
|
+
> **Early draft; experimental tooling.** The model concepts and illustrative
|
|
10
|
+
> fields remain provisional. The library and command are unreleased;
|
|
11
|
+
> there is no universal schema, exporter or deployment engine yet. The Researcher
|
|
12
|
+
> skill supports working research through the host. The standalone command now
|
|
13
|
+
> supports bounded Codex research with numbered Markdown saves and fresh-session reopening.
|
|
14
|
+
|
|
15
|
+
## Why this is needed
|
|
16
|
+
|
|
17
|
+
An AI model's name does not tell you which services an application calls, how
|
|
18
|
+
authentication is configured, or which setup produced an Experiment result.
|
|
19
|
+
Answering those questions can mean piecing together diagrams, configuration files
|
|
20
|
+
and notes, often with incomplete information.
|
|
21
|
+
|
|
22
|
+
The AI Surface Model aims to make that context explicit and shareable. Start with
|
|
23
|
+
the components and connections you know, record the gaps, and add detail as it
|
|
24
|
+
becomes available. A saved description should help someone investigate a system,
|
|
25
|
+
understand a result or assess what they would need to recreate a setup.
|
|
26
|
+
|
|
27
|
+
## Who it is for
|
|
28
|
+
|
|
29
|
+
| If you… | The model is intended to help you… |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| Build or operate AI applications | Describe services, integration settings and dependencies across a setup. |
|
|
32
|
+
| Investigate security or component changes | Find affected components while keeping unknown versions and coverage gaps visible. |
|
|
33
|
+
| Run experiments | Connect results to the definition revision and effective setup actually used. |
|
|
34
|
+
| Build inventory, provenance or analysis tools | Explore exchange of a defined subset with explicit mapping losses. |
|
|
35
|
+
|
|
36
|
+
It is usable independently of Injectionator or any particular application.
|
|
37
|
+
Security investigation, reproducibility and configuration understanding are all
|
|
38
|
+
part of its scope.
|
|
39
|
+
|
|
40
|
+
For a compact security view, start with the provisional
|
|
41
|
+
[Skeleton Surface v0.1](spec/README.md#output-profiles-and-size): key nodes,
|
|
42
|
+
relationships and potential LLM/tool interception points, with linked evidence
|
|
43
|
+
and explicit gaps. Bindings are optional; larger research is not a minimum size.
|
|
44
|
+
|
|
45
|
+
This repository maintains the model and standalone tooling. Application design
|
|
46
|
+
and implementation for `n8r-labs` are maintained in its separate repository.
|
|
47
|
+
|
|
48
|
+
## What it describes
|
|
49
|
+
|
|
50
|
+
An **AI Surface** is the connected system being described: applications, agents,
|
|
51
|
+
AI models, inference services, authentication, tools, data stores and supporting
|
|
52
|
+
infrastructure, together with their configuration and relationships.
|
|
53
|
+
|
|
54
|
+
There is one model: the **AI Surface Model**. A **fragment** is a working term
|
|
55
|
+
for a focused part of a description within it, such as Lab or Experiment; fragment
|
|
56
|
+
boundaries and serialization remain open. A Lab contains Experiments, and each
|
|
57
|
+
Experiment requests its setup directly from the Lab.
|
|
58
|
+
|
|
59
|
+
Definitions stay separate from deployments, dated observations and Experiment
|
|
60
|
+
Runs. A Run refers to the definition revision and effective setup used, so later
|
|
61
|
+
discovery does not silently change what an earlier result refers to.
|
|
62
|
+
|
|
63
|
+
## Experimental tooling
|
|
64
|
+
|
|
65
|
+
The [Researcher library](docs/n8r-surface/contracts.md) reads and writes bounded
|
|
66
|
+
HTML research records and partial definitions in JSON/YAML, with
|
|
67
|
+
[synthetic acceptance cases](examples/researcher/README.md). These checks do not
|
|
68
|
+
establish universal model conformance or consumer compatibility.
|
|
69
|
+
|
|
70
|
+
**To try the command, follow [Install, run and check research](docs/n8r-surface/getting-started.md).**
|
|
71
|
+
|
|
72
|
+
The [bare-startup command](docs/n8r-surface/startup.md) displays local settings and
|
|
73
|
+
saved artefacts, then offers explicitly confirmed local vault setup or reuses the
|
|
74
|
+
saved vault. The explicit `discover [source]` command now starts a
|
|
75
|
+
[Codex research conversation](docs/n8r-surface/codex-research-verification.md)
|
|
76
|
+
with source evidence, follow-up, explicit Markdown saves and fresh-session reopening.
|
|
77
|
+
See [save/reopen evidence and limits](docs/n8r-surface/save-reopen-verification.md).
|
|
78
|
+
The current candidate adds validated paired research HTML and partial Surface JSON,
|
|
79
|
+
with fresh-session revision; see the [candidate evidence and limits](docs/n8r-surface/surface-revisions-verification.md). See the
|
|
80
|
+
[critical path](docs/n8r-surface/critical-path.md) for the next work and the
|
|
81
|
+
[command outcome specification](docs/n8r-surface/command-outcomes.md) for the
|
|
82
|
+
intended interactions and required evidence.
|
|
83
|
+
|
|
84
|
+
The [n8r-surface Researcher skill package](docs/n8r-surface/researcher-skill.md) is now
|
|
85
|
+
available for testing in the existing Codex/ChatGPT harness. It uses the host's
|
|
86
|
+
source access and conversation to produce evidence-backed working notes. The
|
|
87
|
+
[discovery guide](docs/n8r-surface/researcher-discovery.md) focuses on AI-relevant
|
|
88
|
+
graphs, reusable product knowledge and comparing descriptions with evidence. See
|
|
89
|
+
[verification and pending Work-mode checks](docs/n8r-surface/researcher-skill-verification.md).
|
|
90
|
+
The delivery order is skill first, n8r-labs adaptation second, standalone CLI third.
|
|
91
|
+
The [local vault](docs/n8r-surface/researcher-vault.md) holds research notes, intermediate artifacts
|
|
92
|
+
and Surface descriptions at a user-selected local path. It also includes prepared test material
|
|
93
|
+
and a walkthrough for investigating another chat-system repository.
|
|
94
|
+
|
|
95
|
+
## Start at the depth you need
|
|
96
|
+
|
|
97
|
+
The levels are reading paths, not prerequisites. Start at 100 for an introduction,
|
|
98
|
+
or jump to the examples, design questions or contribution guidance.
|
|
99
|
+
|
|
100
|
+
| Level | Start here | What you will find |
|
|
101
|
+
| --- | --- | --- |
|
|
102
|
+
| **100 · Understand** | [Model overview](models/ai-surface-model.md), then [Conceptual guide](models/README.md) | The purpose, vocabulary and a walkthrough of one investigation. |
|
|
103
|
+
| **200 · Explore** | [Synthetic examples](examples/README.md) | Compare descriptions at two depths; identify known facts, gaps and recreation requirements. |
|
|
104
|
+
| **300 · Examine** | [Draft concepts](spec/README.md) and [interoperability research](docs/interoperability.md) | Identity, evidence, partial knowledge, open decisions and proposed mappings to other formats. |
|
|
105
|
+
| **400 · Contribute and extend** | [Propose a change or extension](CONTRIBUTING.md#propose-a-change-or-extension) | Turn a use case into a proposal, example or bounded mapping, then follow the contribution workflow. |
|
|
106
|
+
|
|
107
|
+
The [roadmap](docs/roadmap.md) describes the intended sequence of work and the
|
|
108
|
+
evidence needed before a release.
|
|
109
|
+
|
|
110
|
+
## See progressive detail
|
|
111
|
+
|
|
112
|
+
Both [LibreChat examples](examples/README.md) describe a fictional authentication
|
|
113
|
+
path. These JSON excerpts show the **same component** at two levels of detail.
|
|
114
|
+
Field names are illustrative, not a released schema.
|
|
115
|
+
|
|
116
|
+
In the [minimal description](examples/librechat/minimal.json), its role is known
|
|
117
|
+
but its product is not:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"id": "component:authentication",
|
|
122
|
+
"kind": "service",
|
|
123
|
+
"role": "authentication",
|
|
124
|
+
"product": { "status": "unknown" }
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
In the [expanded description](examples/librechat/expanded.json), the same identity
|
|
129
|
+
has more detail:
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"id": "component:authentication",
|
|
134
|
+
"kind": "service",
|
|
135
|
+
"role": "authentication",
|
|
136
|
+
"product": {
|
|
137
|
+
"status": "known",
|
|
138
|
+
"name": "Keycloak",
|
|
139
|
+
"basis": "synthetic example"
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The expanded file also describes an OpenID Connect relationship and a supporting
|
|
145
|
+
database. Some settings remain unknown or omitted; inference and other application
|
|
146
|
+
dependencies remain unexplored. Neither example establishes a running integration
|
|
147
|
+
or enough information for recreation. They are alternative synthetic descriptions,
|
|
148
|
+
not a revision history or evidence of a deployment change.
|
|
149
|
+
|
|
150
|
+
Detail can grow along any connection, with shared components and cycles. Depth is
|
|
151
|
+
relative to the starting point and has no fixed two-hop ceiling. Missing detail
|
|
152
|
+
does not establish absence.
|
|
153
|
+
|
|
154
|
+
## Working with existing standards and tools
|
|
155
|
+
|
|
156
|
+
We intend to exchange supported subsets through CycloneDX and SPDX, using native
|
|
157
|
+
constructs where suitable and reporting omitted or transformed information.
|
|
158
|
+
Compatibility and round-trip fidelity still need evidence from named versions and
|
|
159
|
+
consuming tools. See the [interoperability research](docs/interoperability.md) for
|
|
160
|
+
mapping questions and related work, including Prismor and sbom-tools.
|
|
161
|
+
|
|
162
|
+
## Contribute or cite
|
|
163
|
+
|
|
164
|
+
Start with an issue, use a dedicated branch and open a linked draft PR; see
|
|
165
|
+
[Contributing](CONTRIBUTING.md) for proposals, checks and the full workflow.
|
|
166
|
+
|
|
167
|
+
Use [CITATION.cff](CITATION.cff) for attribution and include the exact commit URL
|
|
168
|
+
you used when citing this evolving draft.
|
|
169
|
+
|
|
170
|
+
## Licence
|
|
171
|
+
|
|
172
|
+
Machine-readable model assets, schemas, examples, code snippets and tooling use
|
|
173
|
+
[Apache-2.0](LICENSE). Written specification and documentation use
|
|
174
|
+
[CC-BY-4.0](LICENSE-DOCS). See [LICENSING.md](LICENSING.md) for the material split
|
|
175
|
+
and attribution requirements.
|
|
176
|
+
|
|
177
|
+
Commercial and proprietary reuse are welcome under those terms. Users retain
|
|
178
|
+
control of the licensing and publication of their own Surface descriptions and
|
|
179
|
+
Experiment data. Third-party material retains its applicable terms.
|
|
180
|
+
|
|
181
|
+
This is an independent Injectionator project, with no claimed OWASP affiliation
|
|
182
|
+
or endorsement.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Writable } from 'node:stream';
|
|
2
|
+
/** Terminal liveness only: elapsed time does not imply remote progress. */
|
|
3
|
+
export declare class Activity {
|
|
4
|
+
private stream;
|
|
5
|
+
private label;
|
|
6
|
+
private started;
|
|
7
|
+
private frame;
|
|
8
|
+
private timer;
|
|
9
|
+
constructor(stream?: Writable & {
|
|
10
|
+
isTTY?: boolean;
|
|
11
|
+
});
|
|
12
|
+
private clear;
|
|
13
|
+
private draw;
|
|
14
|
+
log(text: string): void;
|
|
15
|
+
run<T>(label: string, action: () => Promise<T>): Promise<T>;
|
|
16
|
+
}
|
package/dist/activity.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Terminal liveness only: elapsed time does not imply remote progress. */
|
|
2
|
+
export class Activity {
|
|
3
|
+
stream;
|
|
4
|
+
label;
|
|
5
|
+
started = 0;
|
|
6
|
+
frame = 0;
|
|
7
|
+
timer;
|
|
8
|
+
constructor(stream = process.stdout) {
|
|
9
|
+
this.stream = stream;
|
|
10
|
+
}
|
|
11
|
+
clear() { if (this.label && this.stream.isTTY)
|
|
12
|
+
this.stream.write('\r\x1b[2K'); }
|
|
13
|
+
draw() {
|
|
14
|
+
if (this.label && this.stream.isTTY) {
|
|
15
|
+
const frames = ['|', '/', '-', '\\'];
|
|
16
|
+
this.stream.write(`\r\x1b[2K${frames[this.frame++ % frames.length]} ${this.label}… ${Math.floor((Date.now() - this.started) / 1000)}s (Ctrl-C to cancel)`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
log(text) {
|
|
20
|
+
this.clear();
|
|
21
|
+
this.stream.write(text);
|
|
22
|
+
this.draw();
|
|
23
|
+
}
|
|
24
|
+
async run(label, action) {
|
|
25
|
+
this.label = label;
|
|
26
|
+
this.started = Date.now();
|
|
27
|
+
this.frame = 0;
|
|
28
|
+
if (this.stream.isTTY) {
|
|
29
|
+
this.draw();
|
|
30
|
+
this.timer = setInterval(() => this.draw(), 250);
|
|
31
|
+
this.timer.unref();
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
return await action();
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (this.timer)
|
|
38
|
+
clearInterval(this.timer);
|
|
39
|
+
this.timer = undefined;
|
|
40
|
+
this.clear();
|
|
41
|
+
this.label = undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { createInterface } from 'node:readline';
|
|
4
|
+
import { overview } from './startup.js';
|
|
5
|
+
import { chooseVault, inspectVault, saveSetup } from './setup.js';
|
|
6
|
+
import { line, problem, loadSettings, localPath, InspectionError } from './settings.js';
|
|
7
|
+
import { SourceTools } from './source-tools.js';
|
|
8
|
+
import { ResearchConversation } from './codex.js';
|
|
9
|
+
import { Activity } from './activity.js';
|
|
10
|
+
import { Notes, readNote, latestNote } from './notes.js';
|
|
11
|
+
import { SurfaceStore, readSurfacePair } from './surface-store.js';
|
|
12
|
+
import { previewSurface } from './surface-draft.js';
|
|
13
|
+
async function setup(config) {
|
|
14
|
+
const existing = config.vault === undefined ? undefined : inspectVault(config.vault);
|
|
15
|
+
if (existing && !existing.missing) {
|
|
16
|
+
console.log(`Vault ready: ${line(existing.root)}`);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
20
|
+
console.log('Vault setup required; run n8r-surface in an interactive terminal.');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
24
|
+
let closed = false;
|
|
25
|
+
input.once('close', () => { closed = true; });
|
|
26
|
+
const cancel = () => input.close();
|
|
27
|
+
input.on('SIGINT', cancel);
|
|
28
|
+
process.on('SIGINT', cancel);
|
|
29
|
+
const ask = (prompt) => new Promise(resolve => {
|
|
30
|
+
if (closed) {
|
|
31
|
+
resolve(undefined);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const ended = () => resolve(undefined);
|
|
35
|
+
input.once('close', ended);
|
|
36
|
+
input.question(prompt, answer => {
|
|
37
|
+
input.removeListener('close', ended);
|
|
38
|
+
resolve(answer);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
let vault = existing?.root;
|
|
43
|
+
if (vault === undefined) {
|
|
44
|
+
const answer = await ask('Vault directory [~/n8r/]: ');
|
|
45
|
+
if (answer === undefined)
|
|
46
|
+
return 1;
|
|
47
|
+
vault = chooseVault(answer === '' ? '~/n8r/' : answer, config.home, process.cwd());
|
|
48
|
+
inspectVault(vault);
|
|
49
|
+
}
|
|
50
|
+
console.log(`Vault directory: ${line(vault)}`);
|
|
51
|
+
if (existing)
|
|
52
|
+
console.log('Required vault folders are missing; confirmation will create them.');
|
|
53
|
+
const answer = await ask('Create vault folders and save this location? [y/N] ');
|
|
54
|
+
if (answer === undefined || !/^y(es)?$/i.test(answer.trim()))
|
|
55
|
+
return 1;
|
|
56
|
+
const saved = saveSetup(config, vault, process.cwd());
|
|
57
|
+
console.log(`Vault ready: ${line(saved)}`);
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
finally {
|
|
61
|
+
input.close();
|
|
62
|
+
process.removeListener('SIGINT', cancel);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const help = `n8r-surface — experimental local AI Surface research
|
|
66
|
+
|
|
67
|
+
n8r-surface Set up or show your local vault
|
|
68
|
+
n8r-surface discover [source] Research a directory or file (default: cwd)
|
|
69
|
+
n8r-surface discover [source] --model MODEL
|
|
70
|
+
n8r-surface reopen [NOTE] --source SOURCE [--model MODEL]
|
|
71
|
+
Continue a note or pair; omit NOTE for latest edited note
|
|
72
|
+
n8r-surface --help Show this help
|
|
73
|
+
n8r-surface --version Show package version
|
|
74
|
+
|
|
75
|
+
Discovery requires a configured vault, an interactive terminal, and Codex CLI
|
|
76
|
+
0.166.0 signed in with ChatGPT. You confirm before source text is sent to OpenAI.
|
|
77
|
+
Uses your configured Codex model unless --model selects one; no fallback.
|
|
78
|
+
Source tools list, search and read bounded UTF-8 text. Hidden entries, dependency
|
|
79
|
+
folders, symlinks, key files and oversized/binary files are excluded.
|
|
80
|
+
No source execution, edits or web access. /save writes numbered Markdown notes.
|
|
81
|
+
Ask for a Surface to preview it; /save surface saves that exact HTML/JSON pair.
|
|
82
|
+
Reopen either research.html or surface.json from a saved pair to revise it.
|
|
83
|
+
Reopen confirms note/source transmission again; no old Codex thread is restored.
|
|
84
|
+
Codex manages its own auth/cache/diagnostics. /exit ends the chat; Ctrl-C cancels.`;
|
|
85
|
+
async function discover(args, reopening = false) {
|
|
86
|
+
let source, model, notePath;
|
|
87
|
+
for (let i = 0; i < args.length; i++) {
|
|
88
|
+
const arg = args[i];
|
|
89
|
+
if (arg === '--model' && model === undefined && args[i + 1] && /^[a-zA-Z0-9._-]{1,100}$/.test(args[i + 1]))
|
|
90
|
+
model = args[++i];
|
|
91
|
+
else if (reopening && arg === '--source' && source === undefined && args[i + 1] && !args[i + 1].startsWith('-'))
|
|
92
|
+
source = args[++i];
|
|
93
|
+
else if (reopening && !arg.startsWith('-') && notePath === undefined)
|
|
94
|
+
notePath = arg;
|
|
95
|
+
else if (!reopening && !arg.startsWith('-') && source === undefined)
|
|
96
|
+
source = arg;
|
|
97
|
+
else
|
|
98
|
+
throw new InspectionError('Unsupported arguments; run n8r-surface discover --help');
|
|
99
|
+
}
|
|
100
|
+
if (reopening && !source)
|
|
101
|
+
throw new InspectionError('Reopen requires --source SOURCE; source access must be selected again');
|
|
102
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
103
|
+
console.log('Discovery requires an interactive terminal; no research started.');
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const config = loadSettings();
|
|
107
|
+
if (!config.vault || inspectVault(config.vault).missing) {
|
|
108
|
+
console.log('Vault setup required; run n8r-surface first.');
|
|
109
|
+
return 1;
|
|
110
|
+
}
|
|
111
|
+
const tools = new SourceTools(localPath(source ?? process.cwd(), config.home, process.cwd()));
|
|
112
|
+
console.log(`Research source: ${line(tools.selected)} (${tools.file ? 'one file' : 'directory'})`);
|
|
113
|
+
console.log('Inspected source text and your messages will be sent to OpenAI through Codex using ChatGPT sign-in.');
|
|
114
|
+
console.log('Source access is read-only. /save saves a Markdown note; /save surface saves a previewed HTML/JSON pair.');
|
|
115
|
+
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
116
|
+
const controller = new AbortController();
|
|
117
|
+
let closed = false, interrupted = false;
|
|
118
|
+
let session;
|
|
119
|
+
input.once('close', () => { closed = true; controller.abort(); });
|
|
120
|
+
const cancel = () => { interrupted = true; controller.abort(); input.close(); };
|
|
121
|
+
input.on('SIGINT', cancel);
|
|
122
|
+
process.on('SIGINT', cancel);
|
|
123
|
+
const ask = (prompt) => new Promise(resolve => {
|
|
124
|
+
if (closed) {
|
|
125
|
+
resolve(undefined);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
const ended = () => resolve(undefined);
|
|
129
|
+
input.once('close', ended);
|
|
130
|
+
input.question(prompt, answer => { input.removeListener('close', ended); resolve(answer); });
|
|
131
|
+
});
|
|
132
|
+
const activity = new Activity();
|
|
133
|
+
const output = (text) => activity.log(text.split('\n').map(part => line(part)).join('\n') + '\n');
|
|
134
|
+
try {
|
|
135
|
+
let note;
|
|
136
|
+
let pair;
|
|
137
|
+
if (reopening && notePath && /(?:research\.html|surface\.json)$/.test(notePath)) {
|
|
138
|
+
pair = readSurfacePair(localPath(notePath, config.home, process.cwd()));
|
|
139
|
+
console.log(`Saved pair: ${line(pair.directory)}`);
|
|
140
|
+
console.log('Both saved artefacts and host evidence will be sent to OpenAI after confirmation.');
|
|
141
|
+
}
|
|
142
|
+
else if (reopening) {
|
|
143
|
+
note = notePath ? readNote(localPath(notePath, config.home, process.cwd())) : latestNote(config.vault, tools.selected);
|
|
144
|
+
if (!note) {
|
|
145
|
+
console.log('No saved numbered note found for this source. Name a note explicitly or start with discover.');
|
|
146
|
+
return 1;
|
|
147
|
+
}
|
|
148
|
+
console.log(`Saved note: ${line(note.path)}`);
|
|
149
|
+
console.log(`Last edited: ${note.modifiedAt}`);
|
|
150
|
+
console.log(`Recorded source: ${line(note.metadata.source.name)} (${note.metadata.source.kind}); this is not verified identity.`);
|
|
151
|
+
if (!notePath) {
|
|
152
|
+
const choice = await ask('Open this note? [y/N] ');
|
|
153
|
+
if (interrupted)
|
|
154
|
+
return 130;
|
|
155
|
+
if (choice === undefined || !/^y(es)?$/i.test(choice.trim()))
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
console.log('The saved note, including human edits, will also be sent to OpenAI after confirmation.');
|
|
159
|
+
}
|
|
160
|
+
const notes = new Notes(config.vault, tools.selected, note);
|
|
161
|
+
const surfaces = new SurfaceStore(config.vault, tools.selected, pair, note);
|
|
162
|
+
const accepted = await ask('Start research with OpenAI using Codex ChatGPT sign-in? [y/N] ');
|
|
163
|
+
if (interrupted)
|
|
164
|
+
return 130;
|
|
165
|
+
if (accepted === undefined || !/^y(es)?$/i.test(accepted.trim()))
|
|
166
|
+
return 1;
|
|
167
|
+
console.log('Connecting to Codex…');
|
|
168
|
+
session = await activity.run('Connecting to Codex', () => ResearchConversation.start(tools, output, model, controller.signal));
|
|
169
|
+
console.log(`Connected: ChatGPT sign-in; model ${line(session.model)}.`);
|
|
170
|
+
console.log(`Researcher ${session.instructions.version}; ${session.instructions.digest}`);
|
|
171
|
+
console.log('Ask about your source or request a Surface preview. /save saves a note; /save surface saves the preview; /exit ends the chat; Ctrl-C cancels.');
|
|
172
|
+
if (note)
|
|
173
|
+
await activity.run('Reopening research', () => session.reopen(note));
|
|
174
|
+
if (pair)
|
|
175
|
+
await activity.run('Reopening Surface', () => session.reopenSurface(pair));
|
|
176
|
+
if (session.surfaceDraft)
|
|
177
|
+
output(previewSurface(session.surfaceDraft));
|
|
178
|
+
while (!closed) {
|
|
179
|
+
const question = await ask('You> ');
|
|
180
|
+
if (question === undefined || question.trim() === '/exit')
|
|
181
|
+
break;
|
|
182
|
+
if (!question.trim())
|
|
183
|
+
continue;
|
|
184
|
+
if (question.trim() === '/save surface') {
|
|
185
|
+
if (!session.surfaceDraft) {
|
|
186
|
+
console.log('No current validated Surface preview. Ask to create or update a Surface first.');
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
const saved = surfaces.save(session.surfaceDraft, { researcher: { version: session.instructions.version, digest: session.instructions.digest }, model: session.model, evidence: session.evidence });
|
|
190
|
+
session.acceptSurface(saved);
|
|
191
|
+
console.log(`Saved and read back: ${line(saved.researchPath)}`);
|
|
192
|
+
console.log(`Saved and read back: ${line(saved.surfacePath)}`);
|
|
193
|
+
}
|
|
194
|
+
else if (question.trim() === '/save') {
|
|
195
|
+
const destination = notes.next();
|
|
196
|
+
console.log(`Save destination: ${line(destination)}`);
|
|
197
|
+
const markdown = await activity.run('Preparing note', () => session.prepareNote());
|
|
198
|
+
if (closed || controller.signal.aborted)
|
|
199
|
+
throw new InspectionError('Research cancelled; nothing saved');
|
|
200
|
+
const saved = notes.save(destination, markdown, { researcher: { version: session.instructions.version, digest: session.instructions.digest }, model: session.model, evidence: session.evidence });
|
|
201
|
+
console.log(`Saved and read back: ${line(saved.path)}`);
|
|
202
|
+
console.log(saved.digest);
|
|
203
|
+
}
|
|
204
|
+
else if (question.trim().startsWith('/')) {
|
|
205
|
+
console.log('Unknown chat command. Use /save, /save surface or /exit.');
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
await activity.run('Waiting for Codex', () => session.turn(question));
|
|
209
|
+
if (session.surfaceDraft)
|
|
210
|
+
output(previewSurface(session.surfaceDraft));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return interrupted ? 130 : 0;
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
if (interrupted) {
|
|
217
|
+
console.log('Research cancelled.');
|
|
218
|
+
return 130;
|
|
219
|
+
}
|
|
220
|
+
if (closed) {
|
|
221
|
+
console.log('Research input closed; answer incomplete.');
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
if (session)
|
|
228
|
+
await session.close();
|
|
229
|
+
input.close();
|
|
230
|
+
process.removeListener('SIGINT', cancel);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
const args = process.argv.slice(2);
|
|
234
|
+
try {
|
|
235
|
+
if ((args.length === 1 && args[0] === '--help') || (args.length === 2 && ['discover', 'reopen'].includes(args[0]) && args[1] === '--help'))
|
|
236
|
+
console.log(help);
|
|
237
|
+
else if (args.length === 1 && args[0] === '--version')
|
|
238
|
+
console.log(`n8r-surface ${JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version}`);
|
|
239
|
+
else if (args[0] === 'reopen')
|
|
240
|
+
process.exitCode = await discover(args.slice(1), true);
|
|
241
|
+
else if (args[0] === 'discover')
|
|
242
|
+
process.exitCode = await discover(args.slice(1));
|
|
243
|
+
else if (args.length) {
|
|
244
|
+
console.log('Unsupported arguments; run n8r-surface --help.');
|
|
245
|
+
process.exitCode = 2;
|
|
246
|
+
}
|
|
247
|
+
else {
|
|
248
|
+
const state = overview();
|
|
249
|
+
console.log(state.text);
|
|
250
|
+
console.log('Setup does not start research. Use n8r-surface discover to begin a conversation.');
|
|
251
|
+
if (state.failed || !state.settings)
|
|
252
|
+
process.exitCode = 2;
|
|
253
|
+
else
|
|
254
|
+
process.exitCode = await setup(state.settings);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
console.log(`n8r-surface failed: ${problem(error)}`);
|
|
259
|
+
process.exitCode = 2;
|
|
260
|
+
}
|
package/dist/codec.d.ts
ADDED
package/dist/codec.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { isAlias, isMap, isNode, isScalar, parseDocument, stringify, visit } from 'yaml';
|
|
2
|
+
import { checkSize, fail, MAX_DEPTH, portable } from './portable.js';
|
|
3
|
+
import { validateArtefact } from './validate.js';
|
|
4
|
+
function formatCheck(format) { if (format !== 'json' && format !== 'yaml')
|
|
5
|
+
fail('$format', 'unsupported encoding'); }
|
|
6
|
+
export function decode(text, format) {
|
|
7
|
+
formatCheck(format);
|
|
8
|
+
checkSize(text);
|
|
9
|
+
if (format === 'json')
|
|
10
|
+
JSON.parse(text); // enforce JSON grammar before YAML's duplicate/number checks
|
|
11
|
+
if (/^%/m.test(text))
|
|
12
|
+
fail('$', 'YAML directives unsupported');
|
|
13
|
+
const doc = parseDocument(text, { version: '1.2', schema: 'core', uniqueKeys: true, strict: true });
|
|
14
|
+
if (doc.errors.length || doc.warnings.length)
|
|
15
|
+
fail('$', [...doc.errors, ...doc.warnings].map(x => x.message).join('; '));
|
|
16
|
+
visit(doc, (_key, node, path) => {
|
|
17
|
+
if (path.length > MAX_DEPTH * 2 + 2)
|
|
18
|
+
fail('$', 'exceeds depth 64');
|
|
19
|
+
if (isAlias(node) || (isNode(node) && (node.tag || ('anchor' in node && node.anchor))))
|
|
20
|
+
fail('$', 'YAML tags/anchors/aliases unsupported');
|
|
21
|
+
if (isMap(node))
|
|
22
|
+
for (const pair of node.items) {
|
|
23
|
+
if (!isScalar(pair.key) || typeof pair.key.value !== 'string')
|
|
24
|
+
fail('$', 'mapping keys must be strings');
|
|
25
|
+
if (format === 'yaml' && pair.key.value === '<<' && pair.key.type !== 'QUOTE_DOUBLE' && pair.key.type !== 'QUOTE_SINGLE')
|
|
26
|
+
fail('$', 'unquoted YAML merge keys unsupported');
|
|
27
|
+
}
|
|
28
|
+
if (isScalar(node) && typeof node.value === 'number' && !/^-?(0|[1-9][0-9]*)$/.test(node.source ?? ''))
|
|
29
|
+
fail('$', 'numbers require decimal integer notation');
|
|
30
|
+
});
|
|
31
|
+
const data = format === 'json' ? JSON.parse(text) : doc.toJS({ maxAliasCount: 0 });
|
|
32
|
+
portable(data);
|
|
33
|
+
return validateArtefact(data);
|
|
34
|
+
}
|
|
35
|
+
export function encode(value, format) {
|
|
36
|
+
formatCheck(format);
|
|
37
|
+
const a = validateArtefact(value);
|
|
38
|
+
const text = format === 'json' ? JSON.stringify(a, null, 2) + '\n' : stringify(a, { version: '1.2', aliasDuplicateObjects: false, lineWidth: 0, defaultKeyType: 'QUOTE_DOUBLE' });
|
|
39
|
+
checkSize(text);
|
|
40
|
+
return text;
|
|
41
|
+
}
|
package/dist/codex.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
import { SourceTools } from './source-tools.js';
|
|
3
|
+
import { type Evidence, type Note } from './notes.js';
|
|
4
|
+
import { type SurfaceDraft } from './surface-draft.js';
|
|
5
|
+
import { type SavedSurfacePair } from './surface-store.js';
|
|
6
|
+
type Wire = Record<string, any>;
|
|
7
|
+
export declare class CodexRpc {
|
|
8
|
+
private child;
|
|
9
|
+
private event;
|
|
10
|
+
private serve;
|
|
11
|
+
private sequence;
|
|
12
|
+
private pending;
|
|
13
|
+
private failure;
|
|
14
|
+
private buffer;
|
|
15
|
+
constructor(child: ChildProcessWithoutNullStreams, event: (method: string, params: Wire) => void, serve: (method: string, params: Wire) => Promise<unknown>);
|
|
16
|
+
private send;
|
|
17
|
+
private receive;
|
|
18
|
+
private fail;
|
|
19
|
+
request(method: string, params: unknown, timeout?: number): Promise<Wire>;
|
|
20
|
+
notify(method: string): void;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export declare function researcherInstructions(): {
|
|
24
|
+
version: string;
|
|
25
|
+
digest: string;
|
|
26
|
+
text: string;
|
|
27
|
+
};
|
|
28
|
+
export declare function runnerConfiguration(current: Wire): Wire;
|
|
29
|
+
export declare class ResearchConversation {
|
|
30
|
+
private source;
|
|
31
|
+
private output;
|
|
32
|
+
private signal?;
|
|
33
|
+
private rpc;
|
|
34
|
+
private scratch;
|
|
35
|
+
private threadId;
|
|
36
|
+
readonly evidence: Evidence[];
|
|
37
|
+
surfaceDraft: SurfaceDraft | undefined;
|
|
38
|
+
private pendingSurface;
|
|
39
|
+
private previousSurface;
|
|
40
|
+
private saving;
|
|
41
|
+
private draft;
|
|
42
|
+
private turnId;
|
|
43
|
+
private earlyEvents;
|
|
44
|
+
private ready;
|
|
45
|
+
private markReady;
|
|
46
|
+
private active;
|
|
47
|
+
private cancelled;
|
|
48
|
+
private closing;
|
|
49
|
+
private abortListener;
|
|
50
|
+
readonly instructions: {
|
|
51
|
+
version: string;
|
|
52
|
+
digest: string;
|
|
53
|
+
text: string;
|
|
54
|
+
};
|
|
55
|
+
model: string;
|
|
56
|
+
private constructor();
|
|
57
|
+
static start(source: SourceTools, output: (text: string) => void, model?: string, signal?: AbortSignal): Promise<ResearchConversation>;
|
|
58
|
+
private event;
|
|
59
|
+
private serve;
|
|
60
|
+
private finish;
|
|
61
|
+
private recordEvidence;
|
|
62
|
+
prepareNote(): Promise<string>;
|
|
63
|
+
acceptSurface(draft: SurfaceDraft): void;
|
|
64
|
+
reopenSurface(pair: SavedSurfacePair): Promise<void>;
|
|
65
|
+
reopen(note: Note): Promise<void>;
|
|
66
|
+
private reopenContext;
|
|
67
|
+
turn(text: string): Promise<void>;
|
|
68
|
+
private runTurn;
|
|
69
|
+
cancel(): Promise<void>;
|
|
70
|
+
close(): Promise<void>;
|
|
71
|
+
}
|
|
72
|
+
export {};
|