@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.
Files changed (96) hide show
  1. package/LICENSE +202 -0
  2. package/LICENSE-DOCS +395 -0
  3. package/LICENSING.md +36 -0
  4. package/README.md +181 -2
  5. package/dist/activity.d.ts +16 -0
  6. package/dist/activity.js +44 -0
  7. package/dist/cli.d.ts +2 -0
  8. package/dist/cli.js +260 -0
  9. package/dist/codec.d.ts +4 -0
  10. package/dist/codec.js +41 -0
  11. package/dist/codex.d.ts +72 -0
  12. package/dist/codex.js +510 -0
  13. package/dist/contracts.d.ts +129 -0
  14. package/dist/contracts.js +1 -0
  15. package/dist/html.d.ts +5 -0
  16. package/dist/html.js +50 -0
  17. package/dist/index.d.ts +7 -0
  18. package/dist/index.js +5 -0
  19. package/dist/notes.d.ts +52 -0
  20. package/dist/notes.js +184 -0
  21. package/dist/portable.d.ts +10 -0
  22. package/dist/portable.js +71 -0
  23. package/dist/revisions.d.ts +2 -0
  24. package/dist/revisions.js +18 -0
  25. package/dist/settings.d.ts +23 -0
  26. package/dist/settings.js +100 -0
  27. package/dist/setup.d.ts +9 -0
  28. package/dist/setup.js +161 -0
  29. package/dist/source-tools.d.ts +102 -0
  30. package/dist/source-tools.js +201 -0
  31. package/dist/startup.d.ts +7 -0
  32. package/dist/startup.js +74 -0
  33. package/dist/surface-draft.d.ts +19 -0
  34. package/dist/surface-draft.js +95 -0
  35. package/dist/surface-instructions.d.ts +2 -0
  36. package/dist/surface-instructions.js +23 -0
  37. package/dist/surface-store.d.ts +36 -0
  38. package/dist/surface-store.js +181 -0
  39. package/dist/validate.d.ts +4 -0
  40. package/dist/validate.js +184 -0
  41. package/docs/n8r-surface/README.md +133 -0
  42. package/docs/n8r-surface/codex-research-transcript.md +51 -0
  43. package/docs/n8r-surface/codex-research-verification.md +176 -0
  44. package/docs/n8r-surface/command-outcomes.md +564 -0
  45. package/docs/n8r-surface/contracts.md +210 -0
  46. package/docs/n8r-surface/critical-path.md +163 -0
  47. package/docs/n8r-surface/design.md +289 -0
  48. package/docs/n8r-surface/getting-started.md +107 -0
  49. package/docs/n8r-surface/host-contract.md +176 -0
  50. package/docs/n8r-surface/implementation-decisions.md +96 -0
  51. package/docs/n8r-surface/npm-distribution.md +137 -0
  52. package/docs/n8r-surface/npm-release.md +188 -0
  53. package/docs/n8r-surface/recreation-handoff.md +117 -0
  54. package/docs/n8r-surface/researcher-discovery.md +157 -0
  55. package/docs/n8r-surface/researcher-prepared-check.md +117 -0
  56. package/docs/n8r-surface/researcher-reconciliation-trial.md +149 -0
  57. package/docs/n8r-surface/researcher-repository-check.md +95 -0
  58. package/docs/n8r-surface/researcher-skeleton-check.md +84 -0
  59. package/docs/n8r-surface/researcher-skill-verification.md +725 -0
  60. package/docs/n8r-surface/researcher-skill.md +255 -0
  61. package/docs/n8r-surface/researcher-vault.md +134 -0
  62. package/docs/n8r-surface/save-reopen-transcript.md +211 -0
  63. package/docs/n8r-surface/save-reopen-verification.md +152 -0
  64. package/docs/n8r-surface/startup-human-check.md +172 -0
  65. package/docs/n8r-surface/startup-verification.md +161 -0
  66. package/docs/n8r-surface/startup.md +151 -0
  67. package/docs/n8r-surface/surface-revisions-transcript.md +552 -0
  68. package/docs/n8r-surface/surface-revisions-verification.md +161 -0
  69. package/docs/n8r-surface/vault-setup-verification.md +210 -0
  70. package/docs/n8r-surface/verification.md +117 -0
  71. package/docs/n8r-surface/versioning.md +183 -0
  72. package/examples/researcher/README.md +92 -0
  73. package/examples/researcher/composed.surface.json +430 -0
  74. package/examples/researcher/composed.surface.yaml +283 -0
  75. package/examples/researcher/harbor-r1.research.html +193 -0
  76. package/examples/researcher/harbor-r2.research.html +226 -0
  77. package/examples/researcher/lumen-manual-edit.pending.html +271 -0
  78. package/examples/researcher/lumen-r1.research.html +271 -0
  79. package/examples/researcher/lumen-r2.research.html +280 -0
  80. package/examples/researcher/sources/harbor-api.md +16 -0
  81. package/examples/researcher/sources/harbor-auth.md +8 -0
  82. package/examples/researcher/sources/lumen-guide.md +28 -0
  83. package/examples/researcher/sources/lumen-settings.md +9 -0
  84. package/package.json +45 -4
  85. package/plugins/n8r-surface/.codex-plugin/plugin.json +31 -0
  86. package/plugins/n8r-surface/LICENSE +202 -0
  87. package/plugins/n8r-surface/LICENSE-DOCS +395 -0
  88. package/plugins/n8r-surface/LICENSING.md +36 -0
  89. package/plugins/n8r-surface/assets/icon.svg +5 -0
  90. package/plugins/n8r-surface/plugin.json +27 -0
  91. package/plugins/n8r-surface/skills/research/SKILL.md +122 -0
  92. package/plugins/n8r-surface/skills/research/agents/openai.yaml +4 -0
  93. package/plugins/n8r-surface/skills/research/references/discovery-map.md +160 -0
  94. package/plugins/n8r-surface/skills/research/references/model-meanings.md +55 -0
  95. package/plugins/n8r-surface/skills/research/references/skeleton-surface.md +105 -0
  96. package/plugins/n8r-surface/skills/research/references/working-notes.md +116 -0
package/README.md CHANGED
@@ -1,3 +1,182 @@
1
- # Temporary Holding Version
1
+ # n8r-surface
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
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
+ }
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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
+ }
@@ -0,0 +1,4 @@
1
+ import type { Artefact } from './contracts.js';
2
+ export type Encoding = 'json' | 'yaml';
3
+ export declare function decode(text: string, format: Encoding): Artefact;
4
+ export declare function encode(value: unknown, format: Encoding): string;
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
+ }
@@ -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 {};