cortex66 0.0.0-stage → 0.3.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/LICENSE +14 -0
- package/README.md +78 -2
- package/agents/planner.md +17 -0
- package/agents/researcher.md +17 -0
- package/agents/reviewer.md +17 -0
- package/dist/adapters/AgentFileStoreAdapter.js +53 -0
- package/dist/adapters/AppConfigAdapterFactory.js +33 -0
- package/dist/adapters/ClaudeCodeConfigAdapter.js +2 -0
- package/dist/adapters/ClaudeDesktopConfigAdapter.js +2 -0
- package/dist/adapters/CodexConfigAdapter.js +2 -0
- package/dist/adapters/ConfigFileWriter.js +37 -0
- package/dist/adapters/CursorConfigAdapter.js +2 -0
- package/dist/adapters/GeminiCliConfigAdapter.js +2 -0
- package/dist/adapters/JsonConfigFileAdapter.js +68 -0
- package/dist/adapters/NoConfigAdapter.js +22 -0
- package/dist/adapters/StateFileAdapter.js +27 -0
- package/dist/adapters/TomlConfigFileAdapter.js +71 -0
- package/dist/adapters/VsCodeConfigAdapter.js +2 -0
- package/dist/commands/CliPorts.js +1 -0
- package/dist/commands/CliProgramFactory.js +55 -0
- package/dist/commands/ConnectCommand.js +39 -0
- package/dist/commands/DisconnectCommand.js +72 -0
- package/dist/commands/DoctorCommand.js +13 -0
- package/dist/commands/StatusCommand.js +50 -0
- package/dist/infrastructure/HttpCortexApiAdapter.js +92 -0
- package/dist/infrastructure/KeychainTokenStoreAdapter.js +40 -0
- package/dist/infrastructure/NodeFileSystemAdapter.js +41 -0
- package/dist/infrastructure/NodeLineReaderAdapter.js +81 -0
- package/dist/infrastructure/NodeProcessAdapter.js +27 -0
- package/dist/infrastructure/TerminalPromptAdapter.js +36 -0
- package/dist/main.js +41 -0
- package/dist/ports/AppConfigPort.js +1 -0
- package/dist/ports/CortexApiPort.js +1 -0
- package/dist/ports/FileSystemPort.js +1 -0
- package/dist/ports/LineReaderPort.js +1 -0
- package/dist/ports/ProcessPort.js +1 -0
- package/dist/ports/PromptPort.js +1 -0
- package/dist/ports/TokenStorePort.js +6 -0
- package/dist/rules/AgentFileMapping.js +29 -0
- package/dist/rules/AgentFileValidator.js +28 -0
- package/dist/rules/AgentProposalPolicy.js +19 -0
- package/dist/rules/AppCatalog.js +54 -0
- package/dist/rules/AppDetectionPolicy.js +38 -0
- package/dist/rules/ComputerPairingStates.js +14 -0
- package/dist/rules/ConfigBackupMemento.js +29 -0
- package/dist/rules/ConnectorEntryPolicy.js +46 -0
- package/dist/rules/DeviceSignInStep.js +74 -0
- package/dist/rules/InstallStepChain.js +114 -0
- package/dist/rules/JsonEditPolicy.js +59 -0
- package/dist/rules/LimitLinePolicy.js +30 -0
- package/dist/rules/SlashCommandCatalog.js +22 -0
- package/dist/rules/TerminalLineParser.js +21 -0
- package/dist/rules/TomlEditPolicy.js +46 -0
- package/dist/terminal/AgentRunService.js +62 -0
- package/dist/terminal/AgentsSlashHandler.js +45 -0
- package/dist/terminal/DoctorService.js +43 -0
- package/dist/terminal/RunSlashHandler.js +46 -0
- package/dist/terminal/SlashCommandRouter.js +78 -0
- package/dist/terminal/TerminalLoop.js +51 -0
- package/dist/terminal/TerminalSession.js +17 -0
- package/dist/terminal/TurnService.js +53 -0
- package/package.json +42 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Cortex66 CLI — proprietary licence
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yotam Elia (Cortex66). All rights reserved.
|
|
4
|
+
|
|
5
|
+
You may install and run this software (the "cortex66" command-line tool) free of charge,
|
|
6
|
+
only to connect to and use the Cortex66 service (https://cortex66.com) under its terms.
|
|
7
|
+
|
|
8
|
+
You may not copy, modify, merge, publish, distribute, sublicense or sell copies of the
|
|
9
|
+
software or its source, except as needed to install and run it as provided, and you may
|
|
10
|
+
not use it to build a competing service.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. IN NO
|
|
13
|
+
EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY ARISING FROM,
|
|
14
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR ITS USE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,79 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cortex66 — Cortex66 in your terminal
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
```bash
|
|
4
|
+
npm i -g cortex66 # Node 20 or newer; nothing runs on install
|
|
5
|
+
cortex66 # type a message; / for commands
|
|
6
|
+
/connect # once, inside: sign this computer in and connect your AI apps
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`cortex66` is a terminal agent like `claude` or `codex`: each line you type is one normal Cortex66 turn (the receipt, your standing cap, the cheapest capable model first, your memory, your data-trust settings) and the answer prints below it. `cortex66 -p "…"` asks once for scripts (exit 1 on a failure). `cortex66 doctor` checks Node, the keychain, that Cortex66 answers, and this computer's token.
|
|
10
|
+
|
|
11
|
+
| Inside `cortex66` | Does |
|
|
12
|
+
|---|---|
|
|
13
|
+
| `/help` · `/exit` | the commands · leave |
|
|
14
|
+
| `/connect` · `/status` · `/disconnect` | the same as the subcommands below (status adds the room) |
|
|
15
|
+
| `/ask <text>` | the same as typing (`//text` sends a line that starts with a slash) |
|
|
16
|
+
| `/model [cheap\|strong]` | the tier; the brain still picks the model inside it |
|
|
17
|
+
| `/cost` | this session's spend (and your standing cap once the server offers it) |
|
|
18
|
+
| `/new` | a new session |
|
|
19
|
+
| `/agents list \| new <name> [--project] \| show <name> \| run <name> <task>` | agents as files (below) |
|
|
20
|
+
| `/plan`, `/auto`, `/room`, `/watch`, `/memory` | wired; the server offers them to computers in the next phase (until then: "not offered yet; nothing was spent") |
|
|
21
|
+
|
|
22
|
+
**Agents** are Markdown files with front matter, the Claude Code custom-agent shape: `.cortex66/agents/<name>.md` in a project wins over `~/.cortex66/agents/<name>.md`, which wins over the starters shipped here (`reviewer`, `planner`, `researcher`).
|
|
23
|
+
|
|
24
|
+
```markdown
|
|
25
|
+
---
|
|
26
|
+
name: reviewer
|
|
27
|
+
description: Reviews a diff for correctness bugs first, then clarity.
|
|
28
|
+
tier: cheap # cheap | strong
|
|
29
|
+
tools: [ask, memory] # ask, plan, auto, memory, room, web
|
|
30
|
+
budget_usd: 0.05 # per session, at most 1; the server caps every turn at your standing cap
|
|
31
|
+
---
|
|
32
|
+
Read the full diff before commenting. …
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
An agent may propose a new agent (a fenced `cortex66-agent` block in its answer). It is checked and written only after you answer y.
|
|
36
|
+
|
|
37
|
+
Publishing (the owner): `npm login`, then from `src/cli`: `npm publish --access public`.
|
|
38
|
+
|
|
39
|
+
One line. It signs this computer in by a device code (your browser opens, you approve; never a password), finds the AI apps on this computer, and, app by app with one key press each, adds the Cortex66 connector to that app — the same connector and the same consent screen the web connect flow uses. Then it offers the Chrome extension and shows what is connected. Everything is reversible.
|
|
40
|
+
|
|
41
|
+
Plan and card: [`Docs/plan/implementation/2026-11-03-desktop-and-smart-install.md`](../../Docs/plan/implementation/2026-11-03-desktop-and-smart-install.md), FL125 in [`Docs/plan/flows/14-desktop-connections-extensions.md`](../../Docs/plan/flows/14-desktop-connections-extensions.md) (D110).
|
|
42
|
+
|
|
43
|
+
## Commands
|
|
44
|
+
|
|
45
|
+
| Command | Does |
|
|
46
|
+
|---|---|
|
|
47
|
+
| `cortex66 connect` | sign in · detect apps · add the connector per app (a yes each) · offer the extension · report. `--dry-run` prints every edit and writes nothing; `--yes` answers only the safe questions (never a config write); `--skip cursor codex` leaves apps alone |
|
|
48
|
+
| `cortex66 status` | sign-in state and, per app, whether the connector is in its file (read from the file, never assumed) |
|
|
49
|
+
| `cortex66 disconnect` | removes exactly what connect added (our entry only, when it still equals what we wrote), revokes this computer's token, forgets the keychain item |
|
|
50
|
+
|
|
51
|
+
## Apps and the files it edits
|
|
52
|
+
|
|
53
|
+
| App | File | Entry |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| Claude Code | `~/.claude.json` | `mcpServers.cortex66 = { type: "http", url }` → then `claude mcp login cortex66` |
|
|
56
|
+
| Cursor | `~/.cursor/mcp.json` | `mcpServers.cortex66 = { url }` |
|
|
57
|
+
| VS Code | `~/Library/Application Support/Code/User/mcp.json` · `%APPDATA%\Code\User\mcp.json` | `servers.cortex66 = { type: "http", url }` |
|
|
58
|
+
| Codex CLI / app | `~/.codex/config.toml` | `[mcp_servers.cortex66] url = …` → then `codex mcp login cortex66` |
|
|
59
|
+
| Gemini CLI | `~/.gemini/settings.json` | `mcpServers.cortex66 = { httpUrl }` |
|
|
60
|
+
| Claude Desktop | `~/Library/Application Support/Claude/claude_desktop_config.json` · `%APPDATA%\Claude\claude_desktop_config.json` | a stdio bridge (`npx -y mcp-remote <url>`); restart the app |
|
|
61
|
+
| ChatGPT desktop | — (no connector file) | the Chrome extension |
|
|
62
|
+
|
|
63
|
+
Before every write: a backup beside the file (`<name>.cortex66-backup-<time>.<ext>`, at most 5 kept), only our key changes, the result is parsed again, and the file is replaced atomically. A file that does not parse is never touched.
|
|
64
|
+
|
|
65
|
+
## What it never does
|
|
66
|
+
|
|
67
|
+
No passwords, no cookies, no provider keys. The computer token lives only in your system keychain. It never reads other apps' windows or keystrokes, never installs anything itself, and never edits a file without your yes.
|
|
68
|
+
|
|
69
|
+
## Develop
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
cd src/cli
|
|
73
|
+
npm ci
|
|
74
|
+
npm run typecheck && npm test # vitest: rules (B1), adapters on real config fixtures (B2), the connect chain with fakes (B3)
|
|
75
|
+
npm run build # tsc → dist/ (bin: dist/main.js)
|
|
76
|
+
CORTEX66_API=http://localhost:5080/api/v1 node dist/main.js status
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Publishing (`npm publish`, name `cortex66`) is the owner's. The sign-in routes (`/auth/computer/*`, `/me/computers/*`) are phase 1b of the plan; until they exist, `connect` stops with "Cortex66 does not offer computer sign-in yet" and writes nothing.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: planner
|
|
3
|
+
description: Turns a goal into a numbered checklist of small reversible steps, each with a completion check. Use before multi-step work.
|
|
4
|
+
tier: cheap
|
|
5
|
+
tools: [ask, memory]
|
|
6
|
+
budget_usd: 0.05
|
|
7
|
+
---
|
|
8
|
+
Restate the goal in one sentence and confirm it matches the user's intent.
|
|
9
|
+
Break the goal into numbered steps small enough to finish and check in one sitting.
|
|
10
|
+
Keep each step reversible. Prefer changes that can be undone with a single command.
|
|
11
|
+
Under each step, write how to check it is done, such as a test result, command output, or file state.
|
|
12
|
+
Name what is unknown, and list the questions that block each unknown.
|
|
13
|
+
Ask before any step that costs money, sends data outside, deletes data, or cannot be undone.
|
|
14
|
+
Mark steps that depend on earlier steps.
|
|
15
|
+
Keep the plan under 15 steps. Split larger goals into phases.
|
|
16
|
+
Do not carry out the steps. Only write the plan.
|
|
17
|
+
Your output is a proposal. The user decides whether and how to follow it.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: researcher
|
|
3
|
+
description: Finds what already exists before proposing anything new. Cites sources and separates facts from guesses.
|
|
4
|
+
tier: cheap
|
|
5
|
+
tools: [ask, memory, web]
|
|
6
|
+
budget_usd: 0.05
|
|
7
|
+
---
|
|
8
|
+
Search for existing tools, code, docs, or decisions that already cover the question before proposing anything.
|
|
9
|
+
Check memory for prior notes on the topic, and say when you use them.
|
|
10
|
+
Use web search only when local sources do not answer the question.
|
|
11
|
+
Cite the source for every fact: a file path with line number, a URL, or a memory entry.
|
|
12
|
+
Label each statement as FACT (with its source) or GUESS (with the reason it is a guess).
|
|
13
|
+
Do not present a guess as a fact, even when it seems obvious.
|
|
14
|
+
List what you searched and what you did not find, so gaps are visible.
|
|
15
|
+
Prefer options that reuse what exists. Describe new work only after that.
|
|
16
|
+
Ask a question when the goal is too vague to search well.
|
|
17
|
+
Your output is a proposal. The user decides which option to take.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Reviews a diff or text for correctness bugs first, then clarity. Use before merging or sending text out.
|
|
4
|
+
tier: cheap
|
|
5
|
+
tools: [ask, memory]
|
|
6
|
+
budget_usd: 0.05
|
|
7
|
+
---
|
|
8
|
+
Read the full diff or text before commenting.
|
|
9
|
+
Look for correctness bugs first: wrong logic, missing error handling, off-by-one errors, race conditions, broken edge cases.
|
|
10
|
+
Then look for clarity problems: unclear names, misleading comments, dead code, confusing structure.
|
|
11
|
+
For each finding, give the severity (high, medium, or low), the line number, and a one-sentence reason.
|
|
12
|
+
Sort findings by severity, highest first.
|
|
13
|
+
If there are no findings, say "no findings" and stop.
|
|
14
|
+
Suggest a fix in a few lines at most. Never rewrite the whole thing.
|
|
15
|
+
Do not praise or summarize the change unless asked.
|
|
16
|
+
Ask before assuming intent when the purpose of the change is unclear.
|
|
17
|
+
Your output is a proposal. The user decides which findings to act on.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Agent files on disk (D125), the Claude Code shape: project `.cortex66/agents/<name>.md` wins over the user's
|
|
2
|
+
// `~/.cortex66/agents/<name>.md`, which wins over the starters shipped in the package (`agents/`). Reads through the FileSystemPort;
|
|
3
|
+
// writes only what AgentFileValidator accepted, never over an existing file.
|
|
4
|
+
import { AgentFileMapping } from "../rules/AgentFileMapping.js";
|
|
5
|
+
import { AgentFileValidator } from "../rules/AgentFileValidator.js";
|
|
6
|
+
export class AgentFileStoreAdapter {
|
|
7
|
+
fs;
|
|
8
|
+
dirs;
|
|
9
|
+
constructor(fs, dirs) {
|
|
10
|
+
this.fs = fs;
|
|
11
|
+
this.dirs = dirs;
|
|
12
|
+
}
|
|
13
|
+
async list() {
|
|
14
|
+
const seen = new Map();
|
|
15
|
+
for (const place of ["project", "user", "starter"]) {
|
|
16
|
+
const dir = this.dirs[place];
|
|
17
|
+
if (!(await this.fs.exists(dir)))
|
|
18
|
+
continue;
|
|
19
|
+
for (const file of (await this.fs.list(dir)).filter((f) => f.endsWith(".md")).sort()) {
|
|
20
|
+
const key = file.slice(0, -3);
|
|
21
|
+
if (!seen.has(key))
|
|
22
|
+
seen.set(key, await this.load(place, `${dir}/${file}`));
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return [...seen.values()];
|
|
26
|
+
}
|
|
27
|
+
async find(name) {
|
|
28
|
+
for (const place of ["project", "user", "starter"]) {
|
|
29
|
+
const path = `${this.dirs[place]}/${name}.md`;
|
|
30
|
+
if (await this.fs.exists(path))
|
|
31
|
+
return this.load(place, path);
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
/** Writes a validated agent; `exists` when a file of that name is already there (nothing is overwritten). */
|
|
36
|
+
async write(agent, place) {
|
|
37
|
+
const v = AgentFileValidator.check(agent);
|
|
38
|
+
if (!v.ok)
|
|
39
|
+
return { refused: "invalid", reason: `${v.field}: ${v.reason}` };
|
|
40
|
+
const dir = this.dirs[place];
|
|
41
|
+
const path = `${dir}/${agent.name}.md`;
|
|
42
|
+
if (await this.fs.exists(path))
|
|
43
|
+
return { refused: "exists", reason: path };
|
|
44
|
+
await this.fs.mkdirp(dir);
|
|
45
|
+
await this.fs.writeAtomic(path, AgentFileMapping.render(agent));
|
|
46
|
+
return { path };
|
|
47
|
+
}
|
|
48
|
+
async load(place, path) {
|
|
49
|
+
const parsed = AgentFileMapping.parse(await this.fs.read(path));
|
|
50
|
+
const verdict = parsed.ok ? AgentFileValidator.check(parsed.fields) : { ok: false, field: "name", reason: "no front matter (--- name … ---)" };
|
|
51
|
+
return { place, path, verdict };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ConnectorEntryPolicy } from "../rules/ConnectorEntryPolicy.js";
|
|
2
|
+
import { ClaudeCodeConfigAdapter } from "./ClaudeCodeConfigAdapter.js";
|
|
3
|
+
import { ClaudeDesktopConfigAdapter } from "./ClaudeDesktopConfigAdapter.js";
|
|
4
|
+
import { CodexConfigAdapter } from "./CodexConfigAdapter.js";
|
|
5
|
+
import { ConfigFileWriter } from "./ConfigFileWriter.js";
|
|
6
|
+
import { CursorConfigAdapter } from "./CursorConfigAdapter.js";
|
|
7
|
+
import { GeminiCliConfigAdapter } from "./GeminiCliConfigAdapter.js";
|
|
8
|
+
import { NoConfigAdapter } from "./NoConfigAdapter.js";
|
|
9
|
+
import { VsCodeConfigAdapter } from "./VsCodeConfigAdapter.js";
|
|
10
|
+
export class AppConfigAdapterFactory {
|
|
11
|
+
deps;
|
|
12
|
+
writer;
|
|
13
|
+
constructor(deps) {
|
|
14
|
+
this.deps = deps;
|
|
15
|
+
this.writer = new ConfigFileWriter(deps.fs, deps.clock);
|
|
16
|
+
}
|
|
17
|
+
for(app) {
|
|
18
|
+
const path = app.configPath(this.deps.paths);
|
|
19
|
+
const entry = ConnectorEntryPolicy.entryFor(app.key, this.deps.connectorUrl);
|
|
20
|
+
if (path === null || entry === null)
|
|
21
|
+
return new NoConfigAdapter(app);
|
|
22
|
+
const d = { fs: this.deps.fs, writer: this.writer, path, entry };
|
|
23
|
+
switch (app.key) {
|
|
24
|
+
case "claude-code": return ClaudeCodeConfigAdapter(app, d);
|
|
25
|
+
case "cursor": return CursorConfigAdapter(app, d);
|
|
26
|
+
case "vscode": return VsCodeConfigAdapter(app, d);
|
|
27
|
+
case "codex": return CodexConfigAdapter(app, d);
|
|
28
|
+
case "gemini-cli": return GeminiCliConfigAdapter(app, d);
|
|
29
|
+
case "claude-desktop": return ClaudeDesktopConfigAdapter(app, d);
|
|
30
|
+
default: return new NoConfigAdapter(app);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ConfigBackupMemento } from "../rules/ConfigBackupMemento.js";
|
|
2
|
+
const dirOf = (p) => p.slice(0, Math.max(p.lastIndexOf("/"), p.lastIndexOf("\\")));
|
|
3
|
+
export class ConfigFileWriter {
|
|
4
|
+
fs;
|
|
5
|
+
clock;
|
|
6
|
+
constructor(fs, clock) {
|
|
7
|
+
this.fs = fs;
|
|
8
|
+
this.clock = clock;
|
|
9
|
+
}
|
|
10
|
+
async write(path, before, after, parses) {
|
|
11
|
+
const reason = parses(after);
|
|
12
|
+
if (reason)
|
|
13
|
+
return { error: `the new content would not parse (${reason}); nothing was written` };
|
|
14
|
+
let backup = null;
|
|
15
|
+
if (before !== null) {
|
|
16
|
+
backup = ConfigBackupMemento.nameFor(path, this.clock.now());
|
|
17
|
+
await this.fs.writeAtomic(backup, before, true);
|
|
18
|
+
await this.pruneBackups(path);
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
await this.fs.mkdirp(dirOf(path));
|
|
22
|
+
}
|
|
23
|
+
await this.fs.writeAtomic(path, after, false);
|
|
24
|
+
return { path, before: before ?? "", after, backup };
|
|
25
|
+
}
|
|
26
|
+
async listBackups(path) {
|
|
27
|
+
const dir = dirOf(path);
|
|
28
|
+
if (!(await this.fs.exists(dir)))
|
|
29
|
+
return [];
|
|
30
|
+
return (await this.fs.list(dir)).filter((n) => ConfigBackupMemento.isBackupOf(path, n)).map((n) => `${dir}/${n}`);
|
|
31
|
+
}
|
|
32
|
+
async pruneBackups(path) {
|
|
33
|
+
const names = (await this.listBackups(path)).map((p) => p.slice(dirOf(path).length + 1));
|
|
34
|
+
for (const old of ConfigBackupMemento.prune(names))
|
|
35
|
+
await this.fs.remove(`${dirOf(path)}/${old}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ConnectorEntryValidator } from "../rules/ConnectorEntryPolicy.js";
|
|
2
|
+
import { JsonEditPolicy } from "../rules/JsonEditPolicy.js";
|
|
3
|
+
export class JsonConfigFileAdapter {
|
|
4
|
+
app;
|
|
5
|
+
path;
|
|
6
|
+
entry;
|
|
7
|
+
fs;
|
|
8
|
+
writer;
|
|
9
|
+
constructor(app, path, entry, fs, writer) {
|
|
10
|
+
this.app = app;
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.entry = entry;
|
|
13
|
+
this.fs = fs;
|
|
14
|
+
this.writer = writer;
|
|
15
|
+
}
|
|
16
|
+
async status() {
|
|
17
|
+
const l = await this.load();
|
|
18
|
+
if ("error" in l)
|
|
19
|
+
return { state: "unparseable", reason: l.error };
|
|
20
|
+
const v = ConnectorEntryValidator.verdict(JsonEditPolicy.get(l.doc, this.app.keyPath), this.entry);
|
|
21
|
+
return v === "ours" ? { state: "connected" } : v === "changed" ? { state: "changed" } : { state: "missing", fileExists: l.text !== null };
|
|
22
|
+
}
|
|
23
|
+
async planConnect() {
|
|
24
|
+
const l = await this.load();
|
|
25
|
+
if ("error" in l)
|
|
26
|
+
return { error: l.error };
|
|
27
|
+
if (ConnectorEntryValidator.verdict(JsonEditPolicy.get(l.doc, this.app.keyPath), this.entry) === "ours")
|
|
28
|
+
return null;
|
|
29
|
+
const after = JsonEditPolicy.serialize(JsonEditPolicy.set(l.doc, this.app.keyPath, this.entry), l.indent);
|
|
30
|
+
return { path: this.path, before: l.text ?? "", after, backup: null };
|
|
31
|
+
}
|
|
32
|
+
async connect() {
|
|
33
|
+
const plan = await this.planConnect();
|
|
34
|
+
if (plan === null || "error" in plan)
|
|
35
|
+
return plan;
|
|
36
|
+
const existed = await this.fs.exists(this.path);
|
|
37
|
+
return this.writer.write(this.path, existed ? plan.before : null, plan.after, parses);
|
|
38
|
+
}
|
|
39
|
+
async planDisconnect() {
|
|
40
|
+
const l = await this.load();
|
|
41
|
+
if ("error" in l)
|
|
42
|
+
return { error: l.error };
|
|
43
|
+
const v = ConnectorEntryValidator.verdict(JsonEditPolicy.get(l.doc, this.app.keyPath), this.entry);
|
|
44
|
+
if (v === "absent")
|
|
45
|
+
return null;
|
|
46
|
+
if (v === "changed")
|
|
47
|
+
return { error: `the cortex66 entry in ${this.path} was changed by hand; I left it alone` };
|
|
48
|
+
const after = JsonEditPolicy.serialize(JsonEditPolicy.remove(l.doc, this.app.keyPath), l.indent);
|
|
49
|
+
return { path: this.path, before: l.text ?? "", after, backup: null };
|
|
50
|
+
}
|
|
51
|
+
async disconnect() {
|
|
52
|
+
const plan = await this.planDisconnect();
|
|
53
|
+
if (plan === null || "error" in plan)
|
|
54
|
+
return plan;
|
|
55
|
+
return this.writer.write(this.path, plan.before, plan.after, parses);
|
|
56
|
+
}
|
|
57
|
+
async load() {
|
|
58
|
+
if (!(await this.fs.exists(this.path)))
|
|
59
|
+
return { text: null, doc: {}, indent: " " };
|
|
60
|
+
const text = await this.fs.read(this.path);
|
|
61
|
+
const p = JsonEditPolicy.parse(text);
|
|
62
|
+
return p.ok ? { text, doc: p.doc, indent: p.indent } : { error: `${this.path} does not parse (${p.reason}); I did not touch it` };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const parses = (text) => {
|
|
66
|
+
const p = JsonEditPolicy.parse(text);
|
|
67
|
+
return p.ok ? null : p.reason;
|
|
68
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export class NoConfigAdapter {
|
|
2
|
+
app;
|
|
3
|
+
path = null;
|
|
4
|
+
constructor(app) {
|
|
5
|
+
this.app = app;
|
|
6
|
+
}
|
|
7
|
+
async status() {
|
|
8
|
+
return { state: "no_config" };
|
|
9
|
+
}
|
|
10
|
+
async planConnect() {
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
async connect() {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
async planDisconnect() {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
async disconnect() {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { JsonEditPolicy } from "../rules/JsonEditPolicy.js";
|
|
2
|
+
const EMPTY = { version: 1, computerId: null, written: {} };
|
|
3
|
+
export class StateFileAdapter {
|
|
4
|
+
fs;
|
|
5
|
+
path;
|
|
6
|
+
constructor(fs, paths) {
|
|
7
|
+
this.fs = fs;
|
|
8
|
+
const dir = paths.platform === "windows" ? `${paths.appData}/cortex66` : `${paths.home}/.config/cortex66`;
|
|
9
|
+
this.path = `${dir}/state.json`;
|
|
10
|
+
}
|
|
11
|
+
async read() {
|
|
12
|
+
if (!(await this.fs.exists(this.path)))
|
|
13
|
+
return { ...EMPTY, written: {} };
|
|
14
|
+
const p = JsonEditPolicy.parse(await this.fs.read(this.path));
|
|
15
|
+
if (!p.ok || p.doc.version !== 1)
|
|
16
|
+
return { ...EMPTY, written: {} };
|
|
17
|
+
return { version: 1, computerId: p.doc.computerId ?? null, written: p.doc.written ?? {} };
|
|
18
|
+
}
|
|
19
|
+
async write(state) {
|
|
20
|
+
await this.fs.mkdirp(this.path.slice(0, this.path.lastIndexOf("/")));
|
|
21
|
+
await this.fs.writeAtomic(this.path, JSON.stringify(state, null, 2) + "\n", true);
|
|
22
|
+
}
|
|
23
|
+
async clear() {
|
|
24
|
+
if (await this.fs.exists(this.path))
|
|
25
|
+
await this.fs.remove(this.path);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ConnectorEntryValidator } from "../rules/ConnectorEntryPolicy.js";
|
|
2
|
+
import { TomlEditPolicy } from "../rules/TomlEditPolicy.js";
|
|
3
|
+
export class TomlConfigFileAdapter {
|
|
4
|
+
app;
|
|
5
|
+
path;
|
|
6
|
+
entry;
|
|
7
|
+
fs;
|
|
8
|
+
writer;
|
|
9
|
+
constructor(app, path, entry, fs, writer) {
|
|
10
|
+
this.app = app;
|
|
11
|
+
this.path = path;
|
|
12
|
+
this.entry = entry;
|
|
13
|
+
this.fs = fs;
|
|
14
|
+
this.writer = writer;
|
|
15
|
+
}
|
|
16
|
+
async status() {
|
|
17
|
+
const l = await this.load();
|
|
18
|
+
if ("error" in l)
|
|
19
|
+
return { state: "unparseable", reason: l.error };
|
|
20
|
+
const v = ConnectorEntryValidator.verdict(TomlEditPolicy.get(l.doc, this.app.keyPath), this.entry);
|
|
21
|
+
return v === "ours" ? { state: "connected" } : v === "changed" ? { state: "changed" } : { state: "missing", fileExists: l.text !== null };
|
|
22
|
+
}
|
|
23
|
+
async planConnect() {
|
|
24
|
+
const l = await this.load();
|
|
25
|
+
if ("error" in l)
|
|
26
|
+
return { error: l.error };
|
|
27
|
+
const v = ConnectorEntryValidator.verdict(TomlEditPolicy.get(l.doc, this.app.keyPath), this.entry);
|
|
28
|
+
if (v === "ours")
|
|
29
|
+
return null;
|
|
30
|
+
if (v === "changed")
|
|
31
|
+
return { error: `${this.path} already has a different [${this.app.keyPath.join(".")}] table; I left it alone` };
|
|
32
|
+
const after = TomlEditPolicy.append(l.text ?? "", this.app.keyPath, this.entry);
|
|
33
|
+
return { path: this.path, before: l.text ?? "", after, backup: null };
|
|
34
|
+
}
|
|
35
|
+
async connect() {
|
|
36
|
+
const plan = await this.planConnect();
|
|
37
|
+
if (plan === null || "error" in plan)
|
|
38
|
+
return plan;
|
|
39
|
+
const existed = await this.fs.exists(this.path);
|
|
40
|
+
return this.writer.write(this.path, existed ? plan.before : null, plan.after, parses);
|
|
41
|
+
}
|
|
42
|
+
async planDisconnect() {
|
|
43
|
+
const l = await this.load();
|
|
44
|
+
if ("error" in l)
|
|
45
|
+
return { error: l.error };
|
|
46
|
+
const v = ConnectorEntryValidator.verdict(TomlEditPolicy.get(l.doc, this.app.keyPath), this.entry);
|
|
47
|
+
if (v === "absent")
|
|
48
|
+
return null;
|
|
49
|
+
if (v === "changed")
|
|
50
|
+
return { error: `the [${this.app.keyPath.join(".")}] table in ${this.path} was changed by hand; I left it alone` };
|
|
51
|
+
const after = TomlEditPolicy.remove(l.text ?? "", this.app.keyPath);
|
|
52
|
+
return { path: this.path, before: l.text ?? "", after, backup: null };
|
|
53
|
+
}
|
|
54
|
+
async disconnect() {
|
|
55
|
+
const plan = await this.planDisconnect();
|
|
56
|
+
if (plan === null || "error" in plan)
|
|
57
|
+
return plan;
|
|
58
|
+
return this.writer.write(this.path, plan.before, plan.after, parses);
|
|
59
|
+
}
|
|
60
|
+
async load() {
|
|
61
|
+
if (!(await this.fs.exists(this.path)))
|
|
62
|
+
return { text: null, doc: {} };
|
|
63
|
+
const text = await this.fs.read(this.path);
|
|
64
|
+
const p = TomlEditPolicy.parse(text);
|
|
65
|
+
return p.ok ? { text, doc: p.doc } : { error: `${this.path} does not parse (${p.reason}); I did not touch it` };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const parses = (text) => {
|
|
69
|
+
const p = TomlEditPolicy.parse(text);
|
|
70
|
+
return p.ok ? null : p.reason;
|
|
71
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// Factory Method (p. 85): builds the `cortex66` program from the ports. `cortex66` alone opens the terminal agent; `-p "<text>"` is
|
|
2
|
+
// one turn for scripts; connect | status | disconnect | doctor stay subcommands. The terminal's /connect, /status and /disconnect run
|
|
3
|
+
// the SAME subcommand code through a fresh program (no second implementation).
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { AgentFileStoreAdapter } from "../adapters/AgentFileStoreAdapter.js";
|
|
6
|
+
import { slashNames } from "../rules/SlashCommandCatalog.js";
|
|
7
|
+
import { TerminalLineParser } from "../rules/TerminalLineParser.js";
|
|
8
|
+
import { AgentRunService } from "../terminal/AgentRunService.js";
|
|
9
|
+
import { AgentsSlashHandler } from "../terminal/AgentsSlashHandler.js";
|
|
10
|
+
import { SlashCommandRouter } from "../terminal/SlashCommandRouter.js";
|
|
11
|
+
import { TerminalLoop } from "../terminal/TerminalLoop.js";
|
|
12
|
+
import { TerminalSession } from "../terminal/TerminalSession.js";
|
|
13
|
+
import { TurnService } from "../terminal/TurnService.js";
|
|
14
|
+
import { registerConnectCommand } from "./ConnectCommand.js";
|
|
15
|
+
import { registerDisconnectCommand } from "./DisconnectCommand.js";
|
|
16
|
+
import { registerDoctorCommand } from "./DoctorCommand.js";
|
|
17
|
+
import { registerStatusCommand } from "./StatusCommand.js";
|
|
18
|
+
const DESCRIPTION = "Cortex66 in your terminal: type a message and the brain answers (receipt, standing cap, cheap first); / for commands.";
|
|
19
|
+
export const CliProgramFactory = {
|
|
20
|
+
build(ports) {
|
|
21
|
+
const program = new Command("cortex66").description(DESCRIPTION).version(ports.version)
|
|
22
|
+
.option("-p, --print <text>", "ask once and print the answer (for scripts); exit code 1 on a failure")
|
|
23
|
+
.action(async (opts) => {
|
|
24
|
+
const loop = CliProgramFactory.loop(ports);
|
|
25
|
+
if (opts.print !== undefined)
|
|
26
|
+
process.exitCode = await loop.print(opts.print);
|
|
27
|
+
else
|
|
28
|
+
await loop.interactive(ports.version);
|
|
29
|
+
});
|
|
30
|
+
CliProgramFactory.subcommands(program, ports);
|
|
31
|
+
registerDoctorCommand(program, ports);
|
|
32
|
+
return program;
|
|
33
|
+
},
|
|
34
|
+
loop(ports) {
|
|
35
|
+
const session = new TerminalSession();
|
|
36
|
+
const turns = new TurnService(ports.api, ports.tokens, session);
|
|
37
|
+
const store = new AgentFileStoreAdapter(ports.fs, ports.agentDirs);
|
|
38
|
+
const reader = ports.lineReader((prefix) => TerminalLineParser.complete(prefix, slashNames()));
|
|
39
|
+
const router = new SlashCommandRouter({
|
|
40
|
+
api: ports.api, turns, session, agents: new AgentsSlashHandler(store, new AgentRunService(turns, store, session)),
|
|
41
|
+
runSubcommand: async (name) => {
|
|
42
|
+
reader.suspend();
|
|
43
|
+
const sub = new Command("cortex66").exitOverride();
|
|
44
|
+
CliProgramFactory.subcommands(sub, ports);
|
|
45
|
+
await sub.parseAsync([name], { from: "user" }).catch((e) => ports.out.warn(`cortex66 ${name}: ${e.message}`));
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
return new TerminalLoop(router, turns, reader, ports.out);
|
|
49
|
+
},
|
|
50
|
+
subcommands(program, ports) {
|
|
51
|
+
registerConnectCommand(program, ports);
|
|
52
|
+
registerStatusCommand(program, ports);
|
|
53
|
+
registerDisconnectCommand(program, ports);
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { AppConfigAdapterFactory } from "../adapters/AppConfigAdapterFactory.js";
|
|
2
|
+
import { StateFileAdapter } from "../adapters/StateFileAdapter.js";
|
|
3
|
+
import { ConnectorEntryPolicy } from "../rules/ConnectorEntryPolicy.js";
|
|
4
|
+
import { InstallStepChain } from "../rules/InstallStepChain.js";
|
|
5
|
+
export const DEFAULT_EXTENSION_URL = "https://cortex66.com/extension";
|
|
6
|
+
export function registerConnectCommand(program, ports) {
|
|
7
|
+
program
|
|
8
|
+
.command("connect")
|
|
9
|
+
.description("sign in by device code, add the Cortex66 connector to the AI apps on this computer (one yes per app), offer the Chrome extension")
|
|
10
|
+
.option("--dry-run", "print every edit, write nothing", false)
|
|
11
|
+
.option("--yes", "answer yes to the safe questions only (never to a config write)", false)
|
|
12
|
+
.option("--skip <app...>", "apps to leave alone (claude-code, cursor, vscode, codex, gemini-cli, claude-desktop)", [])
|
|
13
|
+
.action(async (opts) => {
|
|
14
|
+
const prompt = ports.promptFor(opts.yes);
|
|
15
|
+
const factory = new AppConfigAdapterFactory({ fs: ports.fs, clock: ports.clock, paths: ports.paths, connectorUrl: ports.connectorUrl });
|
|
16
|
+
const ctx = {
|
|
17
|
+
paths: ports.paths, fs: ports.fs, process: ports.process, clock: ports.clock, prompt, api: ports.api, tokens: ports.tokens,
|
|
18
|
+
adapterFor: (app) => factory.for(app),
|
|
19
|
+
options: { dryRun: opts.dryRun, skip: new Set(opts.skip), extensionUrl: ports.extensionUrl ?? DEFAULT_EXTENSION_URL, connectorUrl: ports.connectorUrl },
|
|
20
|
+
};
|
|
21
|
+
const result = await InstallStepChain.run(ctx);
|
|
22
|
+
if (!opts.dryRun && ctx.written && ctx.written.length > 0) {
|
|
23
|
+
const state = new StateFileAdapter(ports.fs, ports.paths);
|
|
24
|
+
const s = await state.read();
|
|
25
|
+
if (ctx.token)
|
|
26
|
+
s.computerId = ctx.token.computerId;
|
|
27
|
+
for (const w of ctx.written) {
|
|
28
|
+
const entry = ConnectorEntryPolicy.entryFor(w.key, ports.connectorUrl);
|
|
29
|
+
if (entry)
|
|
30
|
+
s.written[w.key] = { fingerprint: ConnectorEntryPolicy.fingerprint(entry), path: w.path, at: ports.clock.now().toISOString() };
|
|
31
|
+
}
|
|
32
|
+
await state.write(s);
|
|
33
|
+
}
|
|
34
|
+
if (result.stoppedAt) {
|
|
35
|
+
prompt.warn(`\nstopped at ${result.stoppedAt}: ${result.reason}`);
|
|
36
|
+
process.exitCode = 2;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
}
|