total-recall-brain 3.40.0 → 3.40.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/README.md +152 -41
- package/bin/total-recall.mjs +16 -76
- package/docs/reference/cli-reference.md +10 -10
- package/extension/background.js +6 -2
- package/extension/config.js +8 -0
- package/extension/lib/brain-client.js +29 -4
- package/extension/options/options.html +1 -0
- package/extension/sidepanel/sidepanel.html +1 -0
- package/frontend/dist/assets/{index-CvQRdT2j.css → index-AGUes-VG.css} +1 -1
- package/frontend/dist/assets/index-Bt5iVPZj.js +5618 -0
- package/frontend/dist/index.html +2 -2
- package/frontend/src/api/keys.ts +9 -2
- package/frontend/src/api/plugins.ts +4 -0
- package/frontend/src/components/plugins/card/CardEngine.tsx +2 -1
- package/frontend/src/components/plugins/card/card.css +1 -1
- package/frontend/src/pages/IntegrationsPage.tsx +315 -8
- package/frontend/src/pages/PluginsPage.tsx +532 -87
- package/package.json +6 -5
- package/scaffold/.agent/skills/meta-harness/references/optimized/original.md +1 -1
- package/scaffold/.agent/skills/meta-harness/references/optimized/section-09.md +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +10 -10
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +1 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +0 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/browser-session.mjs +309 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +44 -34
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/google-oauth.mjs +6 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-catalog.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/provider-rotation-recipes.mjs +118 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/routes/auth.mjs +2 -2
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +3 -3
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-rotate.mjs +24 -6
- package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +164 -123
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/README.md +65 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/cli.mjs +76 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/package.json +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +290 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/adapters/dsh.mjs +446 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/documentation.mjs +152 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/executor.mjs +71 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/runtime.mjs +36 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/setup.mjs +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/src/worker.mjs +175 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/ui/card.js +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/README.md +84 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +79 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/check.mjs +6 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/report.mjs +6 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +27 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/snapshot.mjs +163 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +757 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/config.schema.json +86 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/detect.mjs +89 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report-data.mjs +145 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/report.mjs +210 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/state.mjs +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +2869 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +28 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +186 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +272 -0
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +129 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/README.md +73 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/cli.mjs +375 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/echo-tool.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/group.mjs +262 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/examples/commands/project-id.mjs +74 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +2869 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +31 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +221 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/schemas/cli_command.schema.json +107 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/skills/composable-cli/SKILL.md +63 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +313 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compile.mjs +19 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/compose.mjs +144 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/contract.mjs +110 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/library.mjs +23 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/registry.mjs +192 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/resolve.mjs +221 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/risk.mjs +126 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/ssss-documents.mjs +29 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/DESIGN.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +273 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/README.md +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/cli.mjs +310 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +2826 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +62 -0
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +297 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +43 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/cli.mjs +258 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4005 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package.json +20 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +125 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +278 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +9 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +6 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +4721 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +3 -21
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +14 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +10 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/headscale-client.mjs +5 -1
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/leader-election.mjs +4 -13
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-access-verify.mjs +22 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh-files.mjs +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/src/core/mesh.mjs +2 -0
- package/scaffold/.agent/skills/total-recall/plugins/mesh/test-support/memory-root.mjs +14 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +35 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +124 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4097 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +12 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +165 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +127 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +279 -0
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +539 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/LICENSE +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/README.md +67 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +932 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +3256 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/package.json +25 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +297 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +4 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/skills/security/SKILL.md +37 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/src/android.mjs +21 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/src/devices.mjs +94 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/src/machines.mjs +164 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/src/scanner.mjs +41 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/src/windows.mjs +42 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +18 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +95 -0
- package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +98 -0
- package/scaffold/.agent/skills/total-recall/references/auth.md +1 -1
- package/src/cli/backup.mjs +71 -3
- package/src/cli/brief.mjs +33 -9
- package/src/cli/command.mjs +2 -7
- package/src/cli/config.mjs +1 -0
- package/src/cli/connect.mjs +5 -0
- package/src/cli/context.mjs +2 -1
- package/src/cli/daemon.mjs +36 -1
- package/src/cli/doctor.mjs +6 -0
- package/src/cli/harness.mjs +3 -0
- package/src/cli/help.mjs +26 -0
- package/src/cli/init.mjs +4 -4
- package/src/cli/memory-entry.mjs +1 -0
- package/src/cli/memory-init.mjs +2 -0
- package/src/cli/plugin/index.mjs +11 -0
- package/src/cli/plugin/install.mjs +2 -1
- package/src/cli/rebuild.mjs +3 -1
- package/src/cli/startup.mjs +2 -1
- package/src/cli/update.mjs +19 -0
- package/src/core/automatic-brain-updates.mjs +67 -0
- package/src/core/brain-updates.mjs +21 -2
- package/src/core/cli-commands.mjs +71 -0
- package/src/core/cli-maintenance.mjs +45 -0
- package/src/core/cli-quick-reference.mjs +20 -0
- package/src/core/cli-semantic-index.mjs +722 -0
- package/src/core/context-compiler.mjs +27 -1
- package/src/core/crons.mjs +2 -13
- package/src/core/daemon-control.mjs +6 -4
- package/src/core/daemon-loop.mjs +12 -8
- package/src/core/default-plugins.mjs +5 -0
- package/src/core/global-brain-updates.mjs +39 -0
- package/src/core/mesh-brain-updates.mjs +30 -0
- package/src/core/meta-harness.mjs +108 -0
- package/src/core/plugin-dependencies.mjs +2 -1
- package/src/core/plugin-loader.mjs +2 -1
- package/src/core/plugin-runner.mjs +12 -4
- package/src/core/plugin-setup.mjs +20 -0
- package/src/core/plugin-store.mjs +22 -4
- package/src/core/plugin-tasks.mjs +1 -1
- package/src/core/project-brain.mjs +2 -0
- package/src/core/project-status.mjs +64 -0
- package/src/core/required-plugins.mjs +46 -0
- package/src/core/scaffold-plugins.mjs +17 -3
- package/src/core/source-brain-updates.mjs +47 -0
- package/src/core/startup-health.mjs +5 -3
- package/src/core/surface.mjs +44 -10
- package/src/server/routes/extension.mjs +53 -8
- package/templates/onboarding-interview.md +1 -1
- package/frontend/dist/assets/index-SOVUlZqm.js +0 -5596
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Registry — cli_command SSSS documents, history, rollback
|
|
3
|
+
*
|
|
4
|
+
* Each registered command has a sibling <name>.md SSSS document beside its
|
|
5
|
+
* <name>.mjs file. History is kept in <scope dir>/commands/.history/<name>/<version>.mjs.
|
|
6
|
+
*/
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import crypto from 'node:crypto';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
11
|
+
import { EXIT_CODES } from './contract.mjs';
|
|
12
|
+
import { validateRisk } from './risk.mjs';
|
|
13
|
+
import { writeCommandDocument, parseDocument } from './ssss-documents.mjs';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Write the cli_command SSSS document for a command.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} options
|
|
20
|
+
* @param {string} options.name - Command name
|
|
21
|
+
* @param {string} options.commandsDir - Directory containing the command
|
|
22
|
+
* @param {'project'|'group'|'global'} [options.scope]
|
|
23
|
+
* @param {string} [options.ownerProject]
|
|
24
|
+
* @param {string} [options.group]
|
|
25
|
+
* @param {string} [options.risk]
|
|
26
|
+
* @param {number} [options.version]
|
|
27
|
+
* @param {string} [options.author]
|
|
28
|
+
* @param {string} [options.description]
|
|
29
|
+
* @param {boolean} [options.background]
|
|
30
|
+
* @param {string} [options.source]
|
|
31
|
+
* @returns {object} The written registry document
|
|
32
|
+
*/
|
|
33
|
+
export function writeRegistry(options = {}) {
|
|
34
|
+
const {
|
|
35
|
+
name,
|
|
36
|
+
commandsDir,
|
|
37
|
+
scope = 'project',
|
|
38
|
+
ownerProject,
|
|
39
|
+
group,
|
|
40
|
+
risk = 'write',
|
|
41
|
+
version = 1,
|
|
42
|
+
author = process.env.AGENT_ID || 'unknown',
|
|
43
|
+
description = '',
|
|
44
|
+
background = false,
|
|
45
|
+
source = 'inline',
|
|
46
|
+
} = options;
|
|
47
|
+
|
|
48
|
+
const cmdFile = path.join(commandsDir, `${name}.mjs`);
|
|
49
|
+
if (!fs.existsSync(cmdFile)) {
|
|
50
|
+
const err = new Error(`Command '${name}' does not exist in ${commandsDir}.`);
|
|
51
|
+
err.exitCode = EXIT_CODES.ERROR;
|
|
52
|
+
throw err;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
validateRisk(risk);
|
|
56
|
+
|
|
57
|
+
const now = new Date().toISOString();
|
|
58
|
+
const previous = readRegistry(name, commandsDir);
|
|
59
|
+
const code = fs.readFileSync(cmdFile, 'utf8');
|
|
60
|
+
const sha = crypto.createHash('sha256').update(code).digest('hex');
|
|
61
|
+
|
|
62
|
+
const doc = {
|
|
63
|
+
...previous,
|
|
64
|
+
type: 'cli_command',
|
|
65
|
+
id: previous?.id || randomUUID(),
|
|
66
|
+
name,
|
|
67
|
+
scope,
|
|
68
|
+
risk,
|
|
69
|
+
version,
|
|
70
|
+
author,
|
|
71
|
+
created: previous?.created || now,
|
|
72
|
+
updated: now,
|
|
73
|
+
description,
|
|
74
|
+
background_allowed: background,
|
|
75
|
+
code_sha256: sha,
|
|
76
|
+
source,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// Optional fields
|
|
80
|
+
if (ownerProject) doc.owner_project = ownerProject;
|
|
81
|
+
if (group) doc.group = group;
|
|
82
|
+
|
|
83
|
+
const docPath = path.join(commandsDir, `${name}.md`);
|
|
84
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
85
|
+
const prose = fs.existsSync(docPath) ? parseDocument(fs.readFileSync(docPath, 'utf8')).body : `# Command: ${name}\n\n${description}\n`;
|
|
86
|
+
writeCommandDocument(docPath, doc, prose);
|
|
87
|
+
return doc;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Read the cli_command document for a command, if present.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} name
|
|
94
|
+
* @param {string} commandsDir
|
|
95
|
+
* @returns {object | null}
|
|
96
|
+
*/
|
|
97
|
+
export function readRegistry(name, commandsDir) {
|
|
98
|
+
const docPath = path.join(commandsDir, `${name}.md`);
|
|
99
|
+
if (!fs.existsSync(docPath)) return null;
|
|
100
|
+
const content = fs.readFileSync(docPath, 'utf8');
|
|
101
|
+
const doc = parseDocument(content).data;
|
|
102
|
+
// Only return if it's a cli_command
|
|
103
|
+
if (doc.type !== 'cli_command') return null;
|
|
104
|
+
delete doc.description_if_present;
|
|
105
|
+
return doc;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Snapshot the currently registered version of a command before an update.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} name
|
|
112
|
+
* @param {string} commandsDir
|
|
113
|
+
* @returns {number|null} The version number that was snapshotted, or null if none
|
|
114
|
+
*/
|
|
115
|
+
export function snapshotForHistory(name, commandsDir) {
|
|
116
|
+
const cmdFile = path.join(commandsDir, `${name}.mjs`);
|
|
117
|
+
if (!fs.existsSync(cmdFile)) return null;
|
|
118
|
+
|
|
119
|
+
const doc = readRegistry(name, commandsDir);
|
|
120
|
+
const version = doc?.version || 1;
|
|
121
|
+
|
|
122
|
+
const historyDir = path.join(commandsDir, '.history', name);
|
|
123
|
+
fs.mkdirSync(historyDir, { recursive: true });
|
|
124
|
+
|
|
125
|
+
const snapFile = path.join(historyDir, `v${version}.mjs`);
|
|
126
|
+
if (!fs.existsSync(snapFile)) {
|
|
127
|
+
fs.copyFileSync(cmdFile, snapFile);
|
|
128
|
+
}
|
|
129
|
+
return version;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* List history versions for a command.
|
|
134
|
+
*
|
|
135
|
+
* @param {string} name
|
|
136
|
+
* @param {string} commandsDir
|
|
137
|
+
* @returns {number[]} Sorted list of version numbers
|
|
138
|
+
*/
|
|
139
|
+
export function listHistory(name, commandsDir) {
|
|
140
|
+
const histDir = path.join(commandsDir, '.history', name);
|
|
141
|
+
if (!fs.existsSync(histDir)) return [];
|
|
142
|
+
return fs.readdirSync(histDir)
|
|
143
|
+
.filter((f) => /^v\d+\.mjs$/.test(f))
|
|
144
|
+
.map((f) => parseInt(f.replace(/\.mjs$/, '').replace('v', ''), 10))
|
|
145
|
+
.sort((a, b) => a - b);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Roll back a command to a previous version.
|
|
150
|
+
*
|
|
151
|
+
* @param {string} name
|
|
152
|
+
* @param {number} version - Target version to restore
|
|
153
|
+
* @param {string} commandsDir
|
|
154
|
+
* @returns {number} The restored version
|
|
155
|
+
*/
|
|
156
|
+
export function rollback(name, version, commandsDir) {
|
|
157
|
+
const snapFile = path.join(commandsDir, '.history', name, `v${version}.mjs`);
|
|
158
|
+
if (!fs.existsSync(snapFile)) {
|
|
159
|
+
const err = new Error(`Version ${version} of '${name}' does not exist in history.`);
|
|
160
|
+
err.exitCode = EXIT_CODES.ERROR;
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const cmdFile = path.join(commandsDir, `${name}.mjs`);
|
|
165
|
+
if (!fs.existsSync(cmdFile)) {
|
|
166
|
+
const err = new Error(`Command '${name}' does not exist.`);
|
|
167
|
+
err.exitCode = EXIT_CODES.ERROR;
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Snapshot current before rollback
|
|
172
|
+
snapshotForHistory(name, commandsDir);
|
|
173
|
+
|
|
174
|
+
// Restore the target version
|
|
175
|
+
fs.copyFileSync(snapFile, cmdFile);
|
|
176
|
+
|
|
177
|
+
// Bump the registry version to reflect the rollback
|
|
178
|
+
const doc = readRegistry(name, commandsDir);
|
|
179
|
+
if (doc) {
|
|
180
|
+
const newVersion = (doc.version || 0) + 1;
|
|
181
|
+
fm(path.join(commandsDir, `${name}.md`), { ...doc, version: newVersion, updated: new Date().toISOString() });
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return version;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Internal: rewrite a registry doc file with updated fields. */
|
|
188
|
+
function fm(docPath, doc) {
|
|
189
|
+
const body = parseDocument(fs.readFileSync(docPath, 'utf8')).body;
|
|
190
|
+
const code = fs.readFileSync(docPath.replace(/\.md$/, '.mjs'), 'utf8');
|
|
191
|
+
writeCommandDocument(docPath, { ...doc, code_sha256: crypto.createHash('sha256').update(code).digest('hex') }, body);
|
|
192
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command Resolution — scope search, shadow detection, group scope
|
|
3
|
+
*
|
|
4
|
+
* Resolution order for `total-recall <verb>`:
|
|
5
|
+
* 1. Built-in (provided by the total-recall core)
|
|
6
|
+
* 2. Project custom commands (<repo>/.agent/commands/)
|
|
7
|
+
* 3. Group commands (resolved through project_group membership, nearest first)
|
|
8
|
+
* 4. Global custom commands (~/.agent/commands/)
|
|
9
|
+
* 5. VFS integration (<agentDir>/skills/total-recall/integrations/)
|
|
10
|
+
* 6. Plugin manifests whose cli.command matches
|
|
11
|
+
*
|
|
12
|
+
* This module provides:
|
|
13
|
+
* - Shadow detection: refuse built-in names, warn on scope shadowing
|
|
14
|
+
* - Group scope search through project_group membership
|
|
15
|
+
* - Full resolution with provenance
|
|
16
|
+
*/
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { builtinCommandNames } from './command.mjs';
|
|
20
|
+
import { EXIT_CODES } from './contract.mjs';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Find all commands directories for scoped resolution.
|
|
24
|
+
* Returns an ordered array of { scope, dir } objects.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} [options]
|
|
27
|
+
* @param {string} [options.cwd] - Current working directory (default: process.cwd())
|
|
28
|
+
* @param {string[]} [options.groupDirs] - Optional group command directories
|
|
29
|
+
* @returns {{ scope: string, dir: string }[]}
|
|
30
|
+
*/
|
|
31
|
+
export function getCommandTargets(options = {}) {
|
|
32
|
+
const cwd = options.cwd || process.cwd();
|
|
33
|
+
const targets = [];
|
|
34
|
+
|
|
35
|
+
// Project scope
|
|
36
|
+
const projectDir = path.join(cwd, '.agent', 'commands');
|
|
37
|
+
if (fs.existsSync(projectDir)) {
|
|
38
|
+
targets.push({ scope: 'project', dir: projectDir });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Group scope (resolved from project_group documents)
|
|
42
|
+
const groupDirs = options.groupDirs || resolveGroupDirs(cwd);
|
|
43
|
+
for (const gd of groupDirs) {
|
|
44
|
+
if (fs.existsSync(gd)) {
|
|
45
|
+
targets.push({ scope: 'group', dir: gd });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Global scope
|
|
50
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
51
|
+
if (homeDir) {
|
|
52
|
+
const globalDir = path.join(homeDir, '.agent', 'commands');
|
|
53
|
+
if (fs.existsSync(globalDir)) {
|
|
54
|
+
targets.push({ scope: 'global', dir: globalDir });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return targets;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve group command directories from project_group documents.
|
|
63
|
+
* Reads groups from ~/.agent/skills/total-recall/groups/
|
|
64
|
+
* and finds groups that contain this project.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} cwd - Current working directory
|
|
67
|
+
* @returns {string[]} Array of group command directory paths
|
|
68
|
+
*/
|
|
69
|
+
export function resolveGroupDirs(cwd) {
|
|
70
|
+
const dirs = [];
|
|
71
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
72
|
+
if (!homeDir) return dirs;
|
|
73
|
+
|
|
74
|
+
const groupsDir = path.join(homeDir, '.agent', 'skills', 'total-recall', 'groups');
|
|
75
|
+
if (!fs.existsSync(groupsDir)) return dirs;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const projectBrainPath = path.join(cwd, '.agent', 'skills', 'total-recall', 'config', 'brain.json');
|
|
79
|
+
let projectId = null;
|
|
80
|
+
if (fs.existsSync(projectBrainPath)) {
|
|
81
|
+
try {
|
|
82
|
+
const brain = JSON.parse(fs.readFileSync(projectBrainPath, 'utf8'));
|
|
83
|
+
projectId = brain.project_id;
|
|
84
|
+
} catch { /* ignore */ }
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const groupFiles = fs.readdirSync(groupsDir).filter((f) => f.endsWith('.md'));
|
|
88
|
+
for (const gf of groupFiles) {
|
|
89
|
+
try {
|
|
90
|
+
const content = fs.readFileSync(path.join(groupsDir, gf), 'utf8');
|
|
91
|
+
const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
|
|
92
|
+
if (!frontmatter) continue;
|
|
93
|
+
const yaml = frontmatter[1];
|
|
94
|
+
// Simple YAML-like parsing for members
|
|
95
|
+
const membersMatch = yaml.match(/members:\s*\[([^\]]*)\]/);
|
|
96
|
+
if (!membersMatch) continue;
|
|
97
|
+
|
|
98
|
+
const membersStr = membersMatch[1];
|
|
99
|
+
// Check if this project is a member
|
|
100
|
+
const isMember = projectId && membersStr.includes(projectId);
|
|
101
|
+
|
|
102
|
+
if (isMember) {
|
|
103
|
+
const groupCommandsDir = path.join(homeDir, '.agent', 'commands',
|
|
104
|
+
'groups', gf.replace(/\.md$/, ''));
|
|
105
|
+
dirs.push(groupCommandsDir);
|
|
106
|
+
}
|
|
107
|
+
} catch { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
} catch { /* ignore */ }
|
|
110
|
+
|
|
111
|
+
return dirs;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Detect whether a name shadows another command in the resolution chain.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} name - Command name to check
|
|
118
|
+
* @param {object} [options]
|
|
119
|
+
* @param {boolean} [options.throwOnBuiltin=true] - Throw if name is a built-in
|
|
120
|
+
* @returns {{ shadows: string|null, level: string|null }}
|
|
121
|
+
* shadows: the scope whose command of the same name would be shadowed (or null)
|
|
122
|
+
* level: 'builtin' | 'project' | 'group' | 'global' | null
|
|
123
|
+
*/
|
|
124
|
+
export function detectShadow(name, options = {}) {
|
|
125
|
+
const { throwOnBuiltin = true } = options;
|
|
126
|
+
const builtins = builtinCommandNames();
|
|
127
|
+
|
|
128
|
+
// Check built-in (absolute refusal)
|
|
129
|
+
if (builtins.has(name)) {
|
|
130
|
+
if (throwOnBuiltin) {
|
|
131
|
+
const err = new Error(`'${name}' is a built-in Total Recall command; a custom command with that name could never run.`);
|
|
132
|
+
err.exitCode = EXIT_CODES.USAGE;
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
return { shadows: name, level: 'builtin' };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Check project over group/global
|
|
139
|
+
const targets = getCommandTargets();
|
|
140
|
+
for (let i = 0; i < targets.length; i++) {
|
|
141
|
+
const cmdPath = path.join(targets[i].dir, `${name}.mjs`);
|
|
142
|
+
if (fs.existsSync(cmdPath)) {
|
|
143
|
+
// This scope already has the command; if we're creating at a higher
|
|
144
|
+
// priority scope, it shadows the lower one
|
|
145
|
+
if (i > 0) {
|
|
146
|
+
return { shadows: name, level: targets[i].scope };
|
|
147
|
+
}
|
|
148
|
+
// Same scope — already exists error, not shadowing
|
|
149
|
+
return { shadows: null, level: null };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { shadows: null, level: null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Check whether a name would shadow and return a warning if appropriate.
|
|
158
|
+
* Does not throw for built-ins (use detectShadow for that).
|
|
159
|
+
*
|
|
160
|
+
* @param {string} name - Command name to check
|
|
161
|
+
* @returns {string|null} Warning message or null
|
|
162
|
+
*/
|
|
163
|
+
export function shadowWarning(name) {
|
|
164
|
+
// Refuse built-in names with a clear error
|
|
165
|
+
const builtins = builtinCommandNames();
|
|
166
|
+
if (builtins.has(name)) {
|
|
167
|
+
return `'${name}' is a built-in Total Recall command; a custom command with that name could never run.`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const targets = getCommandTargets();
|
|
171
|
+
let found = false;
|
|
172
|
+
for (const t of targets) {
|
|
173
|
+
const cmdPath = path.join(t.dir, `${name}.mjs`);
|
|
174
|
+
if (fs.existsSync(cmdPath)) {
|
|
175
|
+
if (!found) {
|
|
176
|
+
found = true; // First one wins — no warning
|
|
177
|
+
} else {
|
|
178
|
+
return `Warning: '${name}' exists in scope '${t.scope}' but will be shadowed by a higher-priority scope.`;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Resolve a verb through the full scope chain and return its path and source.
|
|
187
|
+
*
|
|
188
|
+
* @param {string} verb - Command name
|
|
189
|
+
* @param {object} [options]
|
|
190
|
+
* @param {string} [options.cwd]
|
|
191
|
+
* @returns {{ path: string|null, scope: string|null }}
|
|
192
|
+
*/
|
|
193
|
+
export function resolveCommand(verb, options = {}) {
|
|
194
|
+
const cwd = options.cwd || process.cwd();
|
|
195
|
+
|
|
196
|
+
// 1. Project
|
|
197
|
+
const projectPath = path.join(cwd, '.agent', 'commands', `${verb}.mjs`);
|
|
198
|
+
if (fs.existsSync(projectPath)) {
|
|
199
|
+
return { path: projectPath, scope: 'project' };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 2. Group (through membership)
|
|
203
|
+
const groupDirs = resolveGroupDirs(cwd);
|
|
204
|
+
for (const gd of groupDirs) {
|
|
205
|
+
const groupPath = path.join(gd, `${verb}.mjs`);
|
|
206
|
+
if (fs.existsSync(groupPath)) {
|
|
207
|
+
return { path: groupPath, scope: 'group' };
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// 3. Global
|
|
212
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
213
|
+
if (homeDir) {
|
|
214
|
+
const globalPath = path.join(homeDir, '.agent', 'commands', `${verb}.mjs`);
|
|
215
|
+
if (fs.existsSync(globalPath)) {
|
|
216
|
+
return { path: globalPath, scope: 'global' };
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return { path: null, scope: null };
|
|
221
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Risk Gate Module
|
|
3
|
+
*
|
|
4
|
+
* Risk classes and the dry-run / --yes gate for composable CLI commands.
|
|
5
|
+
*
|
|
6
|
+
* Risk classes:
|
|
7
|
+
* - read: No side effects. Runs by default.
|
|
8
|
+
* - write: Mutates local files only. Runs by default.
|
|
9
|
+
* - money: Affects financial state. Requires --yes to run.
|
|
10
|
+
* - dns-cert: Changes DNS records or TLS certificates. Requires --yes.
|
|
11
|
+
* - production-deploy: Deploys to production. Requires --yes.
|
|
12
|
+
* - secret-revoke: Rotates/revokes credentials. Requires --yes.
|
|
13
|
+
*
|
|
14
|
+
* Flow:
|
|
15
|
+
* 1. A command declares its risk class in export const meta = { risk: '...' }
|
|
16
|
+
* 2. tr() checks hasRiskGate(risk) before calling the handler
|
|
17
|
+
* 3. If gated and no --yes, calls dryRunResult() to get the plan, returns exit 3
|
|
18
|
+
* 4. With --yes, the handler runs as normal
|
|
19
|
+
* 5. Nested tr() calls inherit dryRun; a gated nested call without --yes
|
|
20
|
+
* fails the parent with exit 3
|
|
21
|
+
*/
|
|
22
|
+
import { EXIT_CODES } from './contract.mjs';
|
|
23
|
+
|
|
24
|
+
/** Risk classes that require --yes to execute */
|
|
25
|
+
export const GATED_RISKS = new Set([
|
|
26
|
+
'money',
|
|
27
|
+
'dns-cert',
|
|
28
|
+
'production-deploy',
|
|
29
|
+
'secret-revoke',
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
/** All valid risk classes */
|
|
33
|
+
export const ALL_RISKS = new Set([
|
|
34
|
+
'read',
|
|
35
|
+
'write',
|
|
36
|
+
...GATED_RISKS,
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check whether a risk class requires the --yes gate.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} risk
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function hasRiskGate(risk) {
|
|
46
|
+
return GATED_RISKS.has(risk);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validate that a risk class is recognized.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} risk
|
|
53
|
+
* @throws {Error} If the risk class is not recognized
|
|
54
|
+
*/
|
|
55
|
+
export function validateRisk(risk) {
|
|
56
|
+
if (!ALL_RISKS.has(risk)) {
|
|
57
|
+
const err = new Error(`Unknown risk class "${risk}". Valid: ${[...ALL_RISKS].join(', ')}`);
|
|
58
|
+
err.exitCode = EXIT_CODES.USAGE;
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
return risk;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run a handler in dry-run mode to obtain a plan.
|
|
66
|
+
* The handler receives { dryRun: true } in its context and should
|
|
67
|
+
* return a description of what it would change.
|
|
68
|
+
*
|
|
69
|
+
* @param {Function} handler - The v2 command handler
|
|
70
|
+
* @param {string[]} args - Arguments
|
|
71
|
+
* @param {object} context - Execution context { verb, json, dryRun, log }
|
|
72
|
+
* @returns {Promise<object>} Plan data
|
|
73
|
+
*/
|
|
74
|
+
export async function dryRunResult(handler, args, context = {}) {
|
|
75
|
+
const { verb, json, dryRun: _, log } = context;
|
|
76
|
+
|
|
77
|
+
// Call the handler with dryRun: true
|
|
78
|
+
const plan = await handler(args, { ...context, dryRun: true });
|
|
79
|
+
|
|
80
|
+
const planOutput = {
|
|
81
|
+
risk: 'gated',
|
|
82
|
+
command: verb,
|
|
83
|
+
args: args.filter((a) => a !== '--yes'),
|
|
84
|
+
plan: plan?.data ?? plan ?? 'No plan returned by command',
|
|
85
|
+
note: 'Run with --yes to execute',
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
if (json) {
|
|
89
|
+
console.log(JSON.stringify({ ok: false, exit_code: EXIT_CODES.ABSTAIN, result: planOutput }));
|
|
90
|
+
} else {
|
|
91
|
+
console.log(` ${verb} would make the following changes:`);
|
|
92
|
+
console.log(JSON.stringify(planOutput.plan, null, 2));
|
|
93
|
+
console.log(`\nPass --yes to execute.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
process.exitCode = EXIT_CODES.ABSTAIN;
|
|
97
|
+
return planOutput;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create an argv parser that looks for --yes and strips it.
|
|
102
|
+
* Also removes --dry-run when dryRun is handled by the context.
|
|
103
|
+
*
|
|
104
|
+
* @param {string[]} args
|
|
105
|
+
* @param {object} [options]
|
|
106
|
+
* @param {boolean} [options.stripYes=true]
|
|
107
|
+
* @param {boolean} [options.stripDryRun=true]
|
|
108
|
+
* @returns {{ cleanArgs: string[], hasYes: boolean }}
|
|
109
|
+
*/
|
|
110
|
+
export function parseRiskArgs(args, { stripYes = true, stripDryRun = true } = {}) {
|
|
111
|
+
let hasYes = false;
|
|
112
|
+
let cleanArgs = args;
|
|
113
|
+
if (stripYes) {
|
|
114
|
+
cleanArgs = cleanArgs.filter((a) => {
|
|
115
|
+
if (a === '--yes' || a === '-y') {
|
|
116
|
+
hasYes = true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
if (stripDryRun) {
|
|
123
|
+
cleanArgs = cleanArgs.filter((a) => a !== '--dry-run');
|
|
124
|
+
}
|
|
125
|
+
return { cleanArgs, hasYes };
|
|
126
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { pathToFileURL } from 'node:url';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
let source;
|
|
7
|
+
if (process.env.TR_PACKAGE_ROOT) {
|
|
8
|
+
const root = path.resolve(process.env.TR_PACKAGE_ROOT);
|
|
9
|
+
if (JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8')).name !== 'total-recall-brain') throw new Error('Invalid native Total Recall host');
|
|
10
|
+
source = path.join(root, 'src/core/plugin-document-store.mjs');
|
|
11
|
+
} else source = require.resolve('total-recall-brain/src/core/plugin-document-store.mjs');
|
|
12
|
+
export const { pluginDocumentOperation, parseDocument, serializeDocument } = await import(pathToFileURL(source).href);
|
|
13
|
+
export const extension = { registry: 'composable-cli', extends: 'ssss', version: '1.0.0', document_primitives: {
|
|
14
|
+
cli_command: { family: 'capability', append_only: false, portability: 'tenant_private',
|
|
15
|
+
required_fields: ['type', 'id', 'name', 'scope', 'risk', 'version', 'code_sha256'], immutable_fields: ['id'],
|
|
16
|
+
patterns: { name: '^[a-z][a-z0-9-]{0,63}$', code_sha256: '^[a-f0-9]{64}$' },
|
|
17
|
+
enums: { scope: ['project', 'group', 'global'], risk: ['read', 'write', 'money', 'dns-cert', 'production-deploy', 'secret-revoke'] } },
|
|
18
|
+
} };
|
|
19
|
+
export function writeCommandDocument(file, metadata, body) {
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
return pluginDocumentOperation({ type: 'operation', path: path.basename(file),
|
|
22
|
+
content: serializeDocument({ ...metadata, title: metadata.title || `Command ${metadata.name}`,
|
|
23
|
+
description: metadata.description || `CLI command ${metadata.name}`, timestamp: now }, body) },
|
|
24
|
+
{ plugin: 'composable-cli', vaultRoot: path.dirname(path.resolve(file)), extension });
|
|
25
|
+
}
|
|
26
|
+
export function deleteCommandDocument(file) {
|
|
27
|
+
return pluginDocumentOperation({ type: 'delete', path: path.basename(file) },
|
|
28
|
+
{ plugin: 'composable-cli', vaultRoot: path.dirname(path.resolve(file)), extension });
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# DESIGN.md — Pipe Dream
|
|
2
|
+
|
|
3
|
+
```yaml
|
|
4
|
+
tokens:
|
|
5
|
+
brand-name: "Pipe Dream"
|
|
6
|
+
wordmark: "[PIPE//DREAM]"
|
|
7
|
+
color-primary: "#a855f7"
|
|
8
|
+
color-background: "#09090b"
|
|
9
|
+
color-accent: "#22c55e"
|
|
10
|
+
badge: "CLI Pipelines"
|
|
11
|
+
font-mono: "'JetBrains Mono', 'Fira Code', monospace"
|
|
12
|
+
border-radius: "8px"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Brand & Typographic Wordmark
|
|
16
|
+
- **Name**: Pipe Dream
|
|
17
|
+
- **Wordmark Logo**: `[PIPE//DREAM]` (custom typographic badge, no generic icons)
|
|
18
|
+
- **Palette**: Primary `#a855f7`, Surface `#09090b`, Accent `#22c55e`.
|