quoxcli 0.4.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +2 -2
- package/bin/quox.js +19 -1
- package/bin/quoxon.js +6 -1
- package/lib/api/endpoints/admin.js +7 -0
- package/lib/api/endpoints/agent-attach.js +7 -0
- package/lib/api/endpoints/compliance.js +25 -0
- package/lib/api/endpoints/governance.js +7 -0
- package/lib/api/endpoints/inbox.js +6 -0
- package/lib/api/endpoints/inboxq.js +58 -0
- package/lib/api/endpoints/index.js +4 -0
- package/lib/api/endpoints/lucid.js +17 -0
- package/lib/api/endpoints/mcp.js +35 -0
- package/lib/api/endpoints/mon.js +122 -0
- package/lib/api/endpoints/notesq.js +2 -0
- package/lib/api/endpoints/quoxplan.js +51 -0
- package/lib/api/endpoints/tools.js +13 -0
- package/lib/api/endpoints/v1.js +13 -0
- package/lib/commands/actions.js +51 -13
- package/lib/commands/admin.js +35 -0
- package/lib/commands/agentAttach.js +15 -0
- package/lib/commands/approvals.js +193 -3
- package/lib/commands/ask.js +1 -1
- package/lib/commands/auth.js +64 -11
- package/lib/commands/backup.js +11 -4
- package/lib/commands/capabilities.js +3 -2
- package/lib/commands/chat.js +15 -0
- package/lib/commands/claude.js +4 -1
- package/lib/commands/compliance.js +109 -0
- package/lib/commands/connect.js +69 -0
- package/lib/commands/contacts.js +2 -0
- package/lib/commands/dev.js +6 -8
- package/lib/commands/doctor.js +22 -3
- package/lib/commands/evidence.js +2 -1
- package/lib/commands/gripe.js +24 -13
- package/lib/commands/inbox.js +37 -3
- package/lib/commands/inboxq.js +147 -0
- package/lib/commands/kuma.js +78 -5
- package/lib/commands/licenceLinkView.js +311 -0
- package/lib/commands/linkedin.js +19 -0
- package/lib/commands/loop.js +30 -3
- package/lib/commands/lucid.js +7 -0
- package/lib/commands/lucidGoals.js +389 -0
- package/lib/commands/mail.js +427 -25
- package/lib/commands/mcp.js +190 -0
- package/lib/commands/memory.js +68 -4
- package/lib/commands/memorySync.js +158 -0
- package/lib/commands/mon.js +69 -144
- package/lib/commands/monitoring.js +68 -0
- package/lib/commands/notes.js +29 -7
- package/lib/commands/notesSeal.js +617 -0
- package/lib/commands/objective.js +5 -2
- package/lib/commands/offload.js +52 -12
- package/lib/commands/plan.js +487 -52
- package/lib/commands/provider.js +29 -8
- package/lib/commands/qlarity.js +32 -1
- package/lib/commands/run.js +2 -1
- package/lib/commands/server.js +37 -0
- package/lib/commands/setup.js +127 -75
- package/lib/commands/skills.js +4 -1
- package/lib/commands/slot.js +49 -9
- package/lib/commands/status.js +92 -44
- package/lib/commands/tool.js +30 -0
- package/lib/commands/vault.js +6 -3
- package/lib/commands/volt.js +6 -1
- package/lib/commands/walkthrough.js +14 -2
- package/lib/commands/watch.js +11 -2
- package/lib/commands/wazuh.js +880 -0
- package/lib/commands/wazuhIntake.js +358 -0
- package/lib/commands/wazuhReport.js +192 -0
- package/lib/commands/workflow.js +47 -4
- package/lib/commands/x.js +50 -0
- package/lib/core/announce.js +86 -0
- package/lib/core/auth.js +20 -2
- package/lib/core/callMe.js +118 -0
- package/lib/core/client.js +27 -1
- package/lib/core/commandRegistrars.js +9 -0
- package/lib/core/config.js +42 -3
- package/lib/core/confirm.js +68 -7
- package/lib/core/crossOrgApprovals.js +69 -0
- package/lib/core/errors.js +52 -1
- package/lib/core/governedSession.js +135 -5
- package/lib/core/inboxResolution.js +3 -2
- package/lib/core/loginGuard.js +73 -0
- package/lib/core/offload.js +120 -0
- package/lib/core/org-override.js +5 -1
- package/lib/core/output.js +30 -5
- package/lib/core/profileTrust.js +182 -0
- package/lib/core/quoxglyph.js +22 -1
- package/lib/core/router.js +14 -0
- package/lib/core/selfUpdate.js +2 -0
- package/lib/core/stepUpPrompt.js +99 -0
- package/lib/core/version.js +1 -1
- package/lib/core/walkthrough.js +9 -5
- package/lib/dragon/select.js +2 -2
- package/lib/quoxon/EXTENSIONS.md +142 -19
- package/lib/quoxon/api.js +189 -34
- package/lib/quoxon/app.js +1277 -188
- package/lib/quoxon/approvalBatch.js +117 -0
- package/lib/quoxon/approvalFacts.js +56 -2
- package/lib/quoxon/approvalSentence.js +8 -4
- package/lib/quoxon/approvalsView.js +72 -12
- package/lib/quoxon/ask.js +4 -2
- package/lib/quoxon/autoOpen.js +94 -0
- package/lib/quoxon/backgroundWork.js +59 -0
- package/lib/quoxon/bang.js +8 -2
- package/lib/quoxon/capabilities.js +7 -2
- package/lib/quoxon/commandShort.js +8 -1
- package/lib/quoxon/commands.js +42 -12
- package/lib/quoxon/connect.js +5 -3
- package/lib/quoxon/deckLazy.js +58 -0
- package/lib/quoxon/delegationDragon.js +49 -0
- package/lib/quoxon/destructive.js +35 -0
- package/lib/quoxon/dnd.js +118 -0
- package/lib/quoxon/dragonCompanion.js +37 -7
- package/lib/quoxon/dragonMoments.js +78 -0
- package/lib/quoxon/echo.js +1 -0
- package/lib/quoxon/events.js +55 -8
- package/lib/quoxon/extensions.js +400 -38
- package/lib/quoxon/foreignText.js +49 -0
- package/lib/quoxon/gripeQueue.js +135 -0
- package/lib/quoxon/inboxView.js +125 -22
- package/lib/quoxon/keys.js +2 -2
- package/lib/quoxon/launcher.js +2 -1
- package/lib/quoxon/loopCard.js +7 -2
- package/lib/quoxon/loopCards.js +17 -8
- package/lib/quoxon/loopStream.js +20 -9
- package/lib/quoxon/mailView.js +71 -0
- package/lib/quoxon/main.js +27 -6
- package/lib/quoxon/markdown.js +1 -1
- package/lib/quoxon/model.js +15 -0
- package/lib/quoxon/neverAnswered.js +20 -0
- package/lib/quoxon/objects/away.js +43 -5
- package/lib/quoxon/onboarding/connectServer.js +162 -0
- package/lib/quoxon/onboarding/copy.js +6 -0
- package/lib/quoxon/onboarding/dockerOffer.js +3 -1
- package/lib/quoxon/onboarding/index.js +5 -43
- package/lib/quoxon/onboarding/probe.js +20 -1
- package/lib/quoxon/onboarding/view.js +16 -3
- package/lib/quoxon/orgSwitch.js +22 -0
- package/lib/quoxon/print.js +2 -0
- package/lib/quoxon/profileSwitch.js +7 -0
- package/lib/quoxon/promptHistory.js +3 -1
- package/lib/quoxon/render.js +68 -16
- package/lib/quoxon/resend.js +14 -0
- package/lib/quoxon/roster.js +10 -5
- package/lib/quoxon/rosterSessions.js +44 -0
- package/lib/quoxon/rows.js +3 -1
- package/lib/quoxon/screenContext.js +1 -1
- package/lib/quoxon/secretShape.js +88 -0
- package/lib/quoxon/slash/agents.js +2 -1
- package/lib/quoxon/slash/argSources.js +9 -2
- package/lib/quoxon/slash/args.js +4 -2
- package/lib/quoxon/slash/broadcast.js +48 -11
- package/lib/quoxon/slash/broadcastRun.js +24 -7
- package/lib/quoxon/slash/cliLeaves.json +98 -1
- package/lib/quoxon/slash/dashboard.js +2 -0
- package/lib/quoxon/slash/hatWalk.js +1 -1
- package/lib/quoxon/slash/listView.js +2 -1
- package/lib/quoxon/slash/palette.js +63 -1
- package/lib/quoxon/slash/paletteController.js +15 -5
- package/lib/quoxon/slash/restart.js +9 -4
- package/lib/quoxon/slash/stepUp.js +5 -5
- package/lib/quoxon/slash/stepUpCmds.js +74 -1
- package/lib/quoxon/slashSpinner.js +35 -0
- package/lib/quoxon/slotAttach.js +8 -0
- package/lib/quoxon/slotChat.js +24 -3
- package/lib/quoxon/slotFormat.js +1 -1
- package/lib/quoxon/slotOutbox.js +113 -0
- package/lib/quoxon/slotRun.js +2 -0
- package/lib/quoxon/suggestCommand.js +1 -1
- package/lib/quoxon/term.js +25 -1
- package/lib/quoxon/transcript.js +2 -1
- package/lib/quoxon/webPty.js +15 -0
- package/lib/quoxon/welcome.js +1 -1
- package/lib/quoxon/whatsnew.js +1 -1
- package/lib/quoxon/whatsnew.jsonl +42 -0
- package/lib/render/planBrief.js +614 -0
- package/lib/server/installFlow.js +168 -0
- package/lib/server/status.js +15 -1
- package/lib/toolOutputShape.js +61 -3
- package/package.json +3 -2
- package/vendor/dragonlab/SOURCE +3 -3
- package/vendor/dragonlab/design/quoxperticus/v2/quoxperticus-master.svg +59 -0
- package/vendor/dragonlab/design/quoxperticus/v3/character.mjs +43 -0
- package/vendor/dragonlab/design/quoxperticus/v4/character.mjs +57 -0
- package/vendor/dragonlab/design/quoxperticus/v4/review.mjs +35 -0
- package/vendor/dragonlab/design/quoxperticus/v4/watch.mjs +56 -0
- package/vendor/dragonlab/design/vulcan/v1/character.mjs +34 -0
- package/vendor/dragonlab/design/vulcan/v1/vulcan-master.svg +57 -0
- package/vendor/dragonlab/design/vulcan/v2/character.mjs +39 -0
- package/vendor/dragonlab/design/vulcan/v3/character.mjs +41 -0
- package/vendor/dragonlab/design/vulcan/v3/review.mjs +35 -0
- package/vendor/dragonlab/design/vulcan/v3/watch.mjs +56 -0
- package/vendor/dragonlab/factory/svg-rig-contour.mjs +64 -0
- package/vendor/dragonlab/integration/companions-v1/characters.mjs +20 -0
- package/vendor/dragonlab/integration/companions-v1/quoxperticus.mjs +2 -0
- package/vendor/dragonlab/integration/companions-v1/view.mjs +1 -1
- package/vendor/dragonlab/integration/companions-v1/vulcan.mjs +2 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QuoxMCP setup from the CLI (VULCAN): the CLI rung under Settings > MCP credentials.
|
|
3
|
+
*
|
|
4
|
+
* quox mcp token create --label <text> [--days <1-90>] mint a personal MCP token (raw token shown ONCE)
|
|
5
|
+
* quox mcp token list your MCP tokens (never the token or its hash)
|
|
6
|
+
* quox mcp token revoke <id> revoke one of your tokens
|
|
7
|
+
* quox mcp bundle [--out <file>] personalised Claude Desktop .mcpb (EMBEDS a fresh token)
|
|
8
|
+
*
|
|
9
|
+
* Tokens: auth /api/auth/tokens/mcp (services/auth/routes/mcp-tokens.js). Bundle: collector /api/v1/mcp/bundle
|
|
10
|
+
* (services/collector/routes/mcp-bundle.js), which takes the raw token in the body and bakes it into the
|
|
11
|
+
* bundle manifest as QUOX_SERVICE_KEY, so the bundle file is a credential: written 0600, never printed.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, writeFileSync, chmodSync } from 'node:fs';
|
|
15
|
+
import { resolve } from 'node:path';
|
|
16
|
+
import { formatOutput, printSuccess, printInfo, printWarning } from '../core/output.js';
|
|
17
|
+
import { resolveEndpoint } from '../api/endpoints.js';
|
|
18
|
+
import { QuoxError } from '../core/errors.js';
|
|
19
|
+
|
|
20
|
+
const MAX_DAYS = 90;
|
|
21
|
+
const OUTPUT_FORMATS = ['table', 'json', 'csv', 'quiet'];
|
|
22
|
+
const DEFAULT_BUNDLE = 'quoxmcp.mcpb';
|
|
23
|
+
|
|
24
|
+
function parseDays(raw) {
|
|
25
|
+
if (raw === undefined) return undefined;
|
|
26
|
+
const s = String(raw).trim();
|
|
27
|
+
const n = Number(s);
|
|
28
|
+
if (!/^\d+$/.test(s) || n < 1 || n > MAX_DAYS) {
|
|
29
|
+
throw new QuoxError(`--days must be a whole number from 1 to ${MAX_DAYS} (got "${raw}")`, 1);
|
|
30
|
+
}
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function parseLabel(raw) {
|
|
35
|
+
const label = String(raw ?? '').trim();
|
|
36
|
+
if (!label) throw new QuoxError('--label must not be empty', 1);
|
|
37
|
+
if (label.length > 128) throw new QuoxError('--label must be 128 characters or fewer', 1);
|
|
38
|
+
return label;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isJson = (program) => program.opts().output === 'json';
|
|
42
|
+
|
|
43
|
+
export function registerMcpCommands(program, ctx) {
|
|
44
|
+
const { client, auth, config } = ctx;
|
|
45
|
+
|
|
46
|
+
async function mint(label, days) {
|
|
47
|
+
const ep = resolveEndpoint('mcp.tokens.create');
|
|
48
|
+
return client.post(ep.path, {
|
|
49
|
+
service: ep.service,
|
|
50
|
+
sensitive: true,
|
|
51
|
+
body: { label, ...(days !== undefined ? { expires_in_days: days } : {}) },
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const mcp = program
|
|
56
|
+
.command('mcp')
|
|
57
|
+
.description('QuoxMCP setup: personal MCP tokens and the Claude Desktop bundle');
|
|
58
|
+
|
|
59
|
+
const token = mcp
|
|
60
|
+
.command('token')
|
|
61
|
+
.description('Personal MCP tokens (qmcp_...) for connecting an MCP client to Quox');
|
|
62
|
+
|
|
63
|
+
token
|
|
64
|
+
.command('create')
|
|
65
|
+
.description(`Mint a personal MCP token for your active org; the raw token is shown ONCE (expires in ${MAX_DAYS} days unless --days is lower)`)
|
|
66
|
+
.requiredOption('--label <text>', 'what this token is for, e.g. "work laptop" (max 128 chars)')
|
|
67
|
+
.option('--days <n>', `days until it expires, 1 to ${MAX_DAYS} (default ${MAX_DAYS})`)
|
|
68
|
+
.action(async (opts) => {
|
|
69
|
+
auth.requireAuth();
|
|
70
|
+
const label = parseLabel(opts.label);
|
|
71
|
+
const days = parseDays(opts.days);
|
|
72
|
+
const data = await mint(label, days);
|
|
73
|
+
if (isJson(program)) {
|
|
74
|
+
console.log(JSON.stringify(data, null, 2));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
printSuccess(`MCP token ${data.id} created (label "${data.label ?? label}").`);
|
|
78
|
+
console.log('Token (shown once, store it now; it cannot be shown again):');
|
|
79
|
+
console.log(data.token);
|
|
80
|
+
console.log(`Expires: ${data.expires_at || 'no expiry'}`);
|
|
81
|
+
printInfo(`Revoke it any time with: quox mcp token revoke ${data.id}`);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
token
|
|
85
|
+
.command('list')
|
|
86
|
+
.description('List your MCP tokens (id, label, created, last used, expiry, revoked); never shows a token value')
|
|
87
|
+
.action(async () => {
|
|
88
|
+
auth.requireAuth();
|
|
89
|
+
const ep = resolveEndpoint('mcp.tokens.list');
|
|
90
|
+
const data = await client.get(ep.path, { service: ep.service });
|
|
91
|
+
const tokens = Array.isArray(data?.tokens) ? data.tokens : [];
|
|
92
|
+
formatOutput({
|
|
93
|
+
columns: ['id', 'label', 'created_at', 'last_used_at', 'expires_at', 'revoked_at'],
|
|
94
|
+
headers: ['ID', 'Label', 'Created', 'Last used', 'Expires', 'Revoked'],
|
|
95
|
+
rows: tokens.map((t) => ({
|
|
96
|
+
id: t.id,
|
|
97
|
+
label: t.label,
|
|
98
|
+
created_at: t.created_at || '',
|
|
99
|
+
last_used_at: t.last_used_at || 'never',
|
|
100
|
+
expires_at: t.expires_at || 'no expiry',
|
|
101
|
+
revoked_at: t.revoked_at || 'no',
|
|
102
|
+
})),
|
|
103
|
+
}, { format: program.opts().output, raw: tokens, emptyMessage: 'No MCP tokens. Create one with: quox mcp token create --label <text>' });
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
token
|
|
107
|
+
.command('revoke')
|
|
108
|
+
.description('Revoke one of your MCP tokens (takes effect at the collector straight away)')
|
|
109
|
+
.argument('<id>', 'token id from `quox mcp token list`')
|
|
110
|
+
.action(async (id) => {
|
|
111
|
+
auth.requireAuth();
|
|
112
|
+
const ep = resolveEndpoint('mcp.tokens.revoke', { id });
|
|
113
|
+
try {
|
|
114
|
+
await client.delete(ep.path, { service: ep.service });
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err?.status === 404) throw new QuoxError(`MCP token ${id} not found or already revoked.`, 1);
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
printSuccess(`MCP token ${id} revoked.`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
mcp
|
|
123
|
+
.command('bundle')
|
|
124
|
+
.description('Download a personalised QuoxMCP bundle (.mcpb) for Claude Desktop. It mints a NEW MCP token and embeds a live MCP token in the file: treat the file as a secret (written 0600, never printed)')
|
|
125
|
+
.option('--out <path>', `where to save it (-o <path> also works); default ./${DEFAULT_BUNDLE}`)
|
|
126
|
+
.option('--label <text>', 'label for the token minted into the bundle', 'Claude Desktop')
|
|
127
|
+
.option('--days <n>', `days until the embedded token expires, 1 to ${MAX_DAYS} (default ${MAX_DAYS})`)
|
|
128
|
+
.option('--collector-url <url>', 'collector URL the bundle will call (default: this profile\'s collector URL)')
|
|
129
|
+
.option('--force', 'overwrite an existing file')
|
|
130
|
+
.addHelpText('after', '\nThe bundle embeds a live MCP token. Anyone holding the file can act as you over MCP until the token\nexpires or you revoke it (quox mcp token revoke <id>). Do not commit or share it.\n')
|
|
131
|
+
.action(async (opts) => {
|
|
132
|
+
auth.requireAuth();
|
|
133
|
+
const label = parseLabel(opts.label);
|
|
134
|
+
const days = parseDays(opts.days);
|
|
135
|
+
const collectorUrl = opts.collectorUrl || config?.getServiceUrl?.('collector');
|
|
136
|
+
try {
|
|
137
|
+
if (!collectorUrl) throw new Error('missing');
|
|
138
|
+
new URL(collectorUrl);
|
|
139
|
+
} catch {
|
|
140
|
+
throw new QuoxError(`--collector-url must be a valid URL (got "${collectorUrl ?? ''}")`, 1);
|
|
141
|
+
}
|
|
142
|
+
const g = program.opts().output;
|
|
143
|
+
const outArg = opts.out || (g && !OUTPUT_FORMATS.includes(g) ? g : undefined) || DEFAULT_BUNDLE;
|
|
144
|
+
const target = resolve(ctx.cwd || process.cwd(), outArg);
|
|
145
|
+
if (existsSync(target) && !opts.force) throw new QuoxError(`${target} already exists. Use --force to overwrite.`, 1);
|
|
146
|
+
|
|
147
|
+
const minted = await mint(label, days);
|
|
148
|
+
const revokeMinted = async () => {
|
|
149
|
+
try {
|
|
150
|
+
const rep = resolveEndpoint('mcp.tokens.revoke', { id: minted.id });
|
|
151
|
+
await client.delete(rep.path, { service: rep.service });
|
|
152
|
+
printWarning(`Revoked the token minted for this bundle (${minted.id}).`);
|
|
153
|
+
} catch {
|
|
154
|
+
printWarning(`Could not revoke the token minted for this bundle; revoke it with: quox mcp token revoke ${minted.id}`);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const bep = resolveEndpoint('mcp.bundle');
|
|
159
|
+
let buf;
|
|
160
|
+
try {
|
|
161
|
+
const res = await client.post(bep.path, {
|
|
162
|
+
service: bep.service,
|
|
163
|
+
raw: true,
|
|
164
|
+
sensitive: true,
|
|
165
|
+
timeout: 120000,
|
|
166
|
+
body: { token: minted.token, collector_url: collectorUrl, agent_id: 'quox' },
|
|
167
|
+
});
|
|
168
|
+
if (!res.ok) {
|
|
169
|
+
let detail = '';
|
|
170
|
+
try { const b = await res.json(); detail = b?.message || b?.error || ''; } catch { /* non-JSON body */ }
|
|
171
|
+
throw new QuoxError(`Bundle generation failed: HTTP ${res.status}${detail ? ` (${detail})` : ''}.`, 1);
|
|
172
|
+
}
|
|
173
|
+
buf = Buffer.from(await res.arrayBuffer());
|
|
174
|
+
} catch (err) {
|
|
175
|
+
await revokeMinted();
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
writeFileSync(target, buf, { mode: 0o600 });
|
|
180
|
+
chmodSync(target, 0o600); // mode only applies on create; --force over an existing file must tighten it too
|
|
181
|
+
if (isJson(program)) {
|
|
182
|
+
console.log(JSON.stringify({ path: target, bytes: buf.length, token_id: minted.id, expires_at: minted.expires_at ?? null, collector_url: collectorUrl }, null, 2));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
printSuccess(`Saved ${target} (${buf.length} bytes, mode 0600).`);
|
|
186
|
+
console.log(`It embeds MCP token ${minted.id} (expires ${minted.expires_at || 'never'}) and calls ${collectorUrl}.`);
|
|
187
|
+
console.log('Open the file with Claude Desktop to install QuoxMCP. Treat it as a secret.');
|
|
188
|
+
printInfo(`Revoke it any time with: quox mcp token revoke ${minted.id}`);
|
|
189
|
+
});
|
|
190
|
+
}
|
package/lib/commands/memory.js
CHANGED
|
@@ -27,12 +27,18 @@ import { QuoxError } from '../core/errors.js';
|
|
|
27
27
|
import { resolveOrgId } from './helpers.js';
|
|
28
28
|
import { resolveMyAttach } from './agentAttach.js';
|
|
29
29
|
import { hasExplicitProfile } from '../core/actingAs.js';
|
|
30
|
+
import { line as glyphLine, printEvent } from '../core/quoxglyph.js';
|
|
31
|
+
import { syncMemoryFiles, expandPaths, defaultMemoryDir } from './memorySync.js';
|
|
32
|
+
import { announce, announceFailure } from '../core/announce.js';
|
|
30
33
|
|
|
31
34
|
// QUOXIDENTITY P9: which agent this session acts as is resolved session-scoped
|
|
32
35
|
// (QUOX_T subtree / QUOX_ATTACH_AGENT override / single-attach fallback), so
|
|
33
36
|
// two sessions on one box no longer collide. See agentAttach.js resolveMyAttach.
|
|
34
37
|
const detectSoleAttachLease = resolveMyAttach;
|
|
35
38
|
|
|
39
|
+
// Short, single-line form of a memory's text for the announce line.
|
|
40
|
+
const clip = (t) => { const x = String(t || '').replace(/\s+/g, ' ').trim(); return x.length > 28 ? x.slice(0, 27) + '…' : x; };
|
|
41
|
+
|
|
36
42
|
/**
|
|
37
43
|
* Call a governed collector memory tool (POST /api/v1/tools/execute).
|
|
38
44
|
* Lease-aware: when exactly one attach lease is active, the call is made
|
|
@@ -161,6 +167,7 @@ export function registerMemoryCommands(program, ctx) {
|
|
|
161
167
|
auth.requireAuth();
|
|
162
168
|
const ep = resolveEndpoint('memory.reinstate', { id });
|
|
163
169
|
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
170
|
+
announce('memory', `reinstated ${data.reinstated?.id || id}`);
|
|
164
171
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
165
172
|
printSuccess(`Reinstated ${data.reinstated?.id}; retired ${data.retired?.id} (marked disputed, Dream will not re-apply it)`);
|
|
166
173
|
});
|
|
@@ -180,10 +187,56 @@ export function registerMemoryCommands(program, ctx) {
|
|
|
180
187
|
};
|
|
181
188
|
if (options.tags) body.tags = options.tags.split(',').map(t => t.trim());
|
|
182
189
|
if (options.agent) body.agent_id = options.agent;
|
|
183
|
-
|
|
190
|
+
const label = `${options.type} "${clip(options.content)}"`;
|
|
191
|
+
let data;
|
|
192
|
+
try {
|
|
193
|
+
data = await client.post(resolveEndpoint('memory.create').path, { service: resolveEndpoint('memory.create').service, body });
|
|
194
|
+
} catch (err) { announceFailure('memory', `save ${label}`, err); throw err; }
|
|
195
|
+
announce('memory', `saved ${label}`, { result: data?.id || data?.memory?.id });
|
|
184
196
|
printSuccess('Memory saved.');
|
|
185
197
|
});
|
|
186
198
|
|
|
199
|
+
// Owner order 2026-10-29: do not replace Claude's file memory, ALSO save it to Quox memory.
|
|
200
|
+
// Additive and idempotent (see memorySync.js). One QuoxGlyph line per file saved or updated.
|
|
201
|
+
memory
|
|
202
|
+
.command('sync')
|
|
203
|
+
.description("Also save Claude Code's file memory (default: this project's memory dir) into Quox memory. Idempotent, additive, secrets refused")
|
|
204
|
+
.argument('[paths...]', 'memory files or directories (default: ~/.claude/projects/<this cwd>/memory)')
|
|
205
|
+
.option('--dry-run', 'show what would be saved without writing')
|
|
206
|
+
.action(async (paths, options) => {
|
|
207
|
+
auth.requireAuth();
|
|
208
|
+
const json = program.opts().output === 'json';
|
|
209
|
+
const targets = paths && paths.length ? paths : [defaultMemoryDir()];
|
|
210
|
+
const files = expandPaths(targets);
|
|
211
|
+
const ep = resolveEndpoint('memory.create');
|
|
212
|
+
const orgId = auth.getOrgId?.(program.opts().org) || 'default';
|
|
213
|
+
const results = await syncMemoryFiles({ client, orgId, files, dryRun: !!options.dryRun, memoryEp: ep });
|
|
214
|
+
const count = (a) => results.filter((r) => r.action === a).length;
|
|
215
|
+
const summary = {
|
|
216
|
+
created: count('created'), updated: count('updated'), unchanged: count('unchanged'),
|
|
217
|
+
refused: count('refused'), errors: count('error') + count('missing'),
|
|
218
|
+
would_create: count('would-create'), would_update: count('would-update'),
|
|
219
|
+
};
|
|
220
|
+
if (json) {
|
|
221
|
+
console.log(JSON.stringify({ dry_run: !!options.dryRun, summary, results }, null, 2));
|
|
222
|
+
} else {
|
|
223
|
+
const q = (r) => `"${r.name}"`;
|
|
224
|
+
for (const r of results) {
|
|
225
|
+
if (r.action === 'created') printEvent(glyphLine('memory', `also saved to Quox ${q(r)}`, { status: 'ok' }));
|
|
226
|
+
else if (r.action === 'updated') printEvent(glyphLine('memory', `updated in Quox ${q(r)}`, { status: 'ok' }));
|
|
227
|
+
else if (r.action === 'would-create') printEvent(glyphLine('memory', `would save to Quox ${q(r)}`, { status: 'busy' }));
|
|
228
|
+
else if (r.action === 'would-update') printEvent(glyphLine('memory', `would update in Quox ${q(r)}`, { status: 'busy' }));
|
|
229
|
+
else if (r.action === 'refused') printEvent(glyphLine('memory', `refused ${r.file.split('/').pop()}`, { result: `secret shape: ${r.kinds.join(', ')}`, status: 'warn' }));
|
|
230
|
+
else if (r.action === 'error' || r.action === 'missing') printEvent(glyphLine('memory', `sync ${r.file.split('/').pop()}`, { result: r.action === 'missing' ? 'file not found' : (r.error || 'failed').slice(0, 40), status: 'fail' }));
|
|
231
|
+
}
|
|
232
|
+
const bits = options.dryRun
|
|
233
|
+
? `${summary.would_create} to save, ${summary.would_update} to update, ${summary.unchanged} unchanged`
|
|
234
|
+
: `${summary.created} saved, ${summary.updated} updated, ${summary.unchanged} unchanged`;
|
|
235
|
+
console.log(`Memory sync: ${bits}${summary.refused ? `, ${summary.refused} refused (secret shapes)` : ''}${summary.errors ? `, ${summary.errors} failed` : ''}.`);
|
|
236
|
+
}
|
|
237
|
+
if (summary.errors) process.exitCode = 1;
|
|
238
|
+
});
|
|
239
|
+
|
|
187
240
|
memory
|
|
188
241
|
.command('update')
|
|
189
242
|
.description('Update a memory')
|
|
@@ -195,7 +248,10 @@ export function registerMemoryCommands(program, ctx) {
|
|
|
195
248
|
const body = {};
|
|
196
249
|
if (options.content) body.content = options.content;
|
|
197
250
|
if (options.tags) body.tags = options.tags.split(',').map(t => t.trim());
|
|
198
|
-
|
|
251
|
+
try {
|
|
252
|
+
await client.patch(resolveEndpoint('memory.update', { id }).path, { service: resolveEndpoint('memory.update', { id }).service, body });
|
|
253
|
+
} catch (err) { announceFailure('memory', `update ${id}`, err); throw err; }
|
|
254
|
+
announce('memory', `updated ${id}`, { result: options.content ? `"${clip(options.content)}"` : undefined });
|
|
199
255
|
printSuccess('Memory updated.');
|
|
200
256
|
});
|
|
201
257
|
|
|
@@ -208,7 +264,10 @@ export function registerMemoryCommands(program, ctx) {
|
|
|
208
264
|
auth.requireAuth();
|
|
209
265
|
const query = {};
|
|
210
266
|
if (options.hard) query.hard = 'true';
|
|
211
|
-
|
|
267
|
+
try {
|
|
268
|
+
await client.delete(resolveEndpoint('memory.delete', { id }).path, { service: resolveEndpoint('memory.delete', { id }).service, query });
|
|
269
|
+
} catch (err) { announceFailure('memory', `delete ${id}`, err); throw err; }
|
|
270
|
+
announce('memory', `deleted ${id}`, { result: options.hard ? 'permanent' : 'soft delete' });
|
|
212
271
|
printSuccess('Memory deleted.');
|
|
213
272
|
});
|
|
214
273
|
|
|
@@ -226,7 +285,10 @@ export function registerMemoryCommands(program, ctx) {
|
|
|
226
285
|
const ep = resolveEndpoint('memory.delete', { id });
|
|
227
286
|
const query = {};
|
|
228
287
|
if (options.permanent) query.hard = 'true';
|
|
229
|
-
|
|
288
|
+
try {
|
|
289
|
+
await client.delete(ep.path, { service: ep.service, query });
|
|
290
|
+
} catch (err) { announceFailure('memory', `forget ${id}`, err); throw err; }
|
|
291
|
+
announce('memory', `forgot ${id}`, { result: options.permanent ? 'erased for good' : 'soft delete' });
|
|
230
292
|
printSuccess(options.permanent ? `Permanently erased memory ${id}.` : `Forgot memory ${id}.`);
|
|
231
293
|
});
|
|
232
294
|
|
|
@@ -333,6 +395,7 @@ export function registerMemoryCommands(program, ctx) {
|
|
|
333
395
|
auth.requireAuth();
|
|
334
396
|
const ep = resolveEndpoint('memory.pin', { id });
|
|
335
397
|
const data = await client.post(ep.path, { service: ep.service, body: {} });
|
|
398
|
+
announce('memory', `pinned ${id}`, { result: 'core set' });
|
|
336
399
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
337
400
|
printSuccess(`Pinned ${id} into the core set.`);
|
|
338
401
|
});
|
|
@@ -345,6 +408,7 @@ export function registerMemoryCommands(program, ctx) {
|
|
|
345
408
|
auth.requireAuth();
|
|
346
409
|
const ep = resolveEndpoint('memory.unpin', { id });
|
|
347
410
|
const data = await client.delete(ep.path, { service: ep.service });
|
|
411
|
+
announce('memory', `unpinned ${id}`, { result: 'out of core set' });
|
|
348
412
|
if (program.opts().output === 'json') { console.log(JSON.stringify(data, null, 2)); return; }
|
|
349
413
|
printSuccess(`Unpinned ${id}: it stays a memory, but is no longer in the core set.`);
|
|
350
414
|
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `quox memory sync`: also save agent file memory (Claude Code's
|
|
3
|
+
* ~/.claude/projects/<slug>/memory/*.md) into Quox memory. ADDITIVE: the Claude
|
|
4
|
+
* files are only read, never written, and a removed file never deletes a Quox
|
|
5
|
+
* memory. Idempotent: each file has a stable source key
|
|
6
|
+
* (claude-memory:<slug>/<file>) and a content hash; unchanged files are skipped,
|
|
7
|
+
* changed files PATCH the same Quox memory. Files containing secret shapes are
|
|
8
|
+
* refused (named, never echoed).
|
|
9
|
+
*/
|
|
10
|
+
import { createHash } from 'node:crypto';
|
|
11
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, renameSync } from 'node:fs';
|
|
12
|
+
import { basename, dirname, join, resolve } from 'node:path';
|
|
13
|
+
import { homedir } from 'node:os';
|
|
14
|
+
import { secretKinds } from '../quoxon/secretShape.js';
|
|
15
|
+
|
|
16
|
+
const MAX_CONTENT = 20000;
|
|
17
|
+
|
|
18
|
+
/** Claude memory type -> closest Quox memory type (the API accepts fact, preference, decision, observation, entity). */
|
|
19
|
+
export function quoxTypeFor(claudeType) {
|
|
20
|
+
switch (String(claudeType || '').toLowerCase()) {
|
|
21
|
+
case 'user': return 'preference';
|
|
22
|
+
case 'feedback': return 'decision';
|
|
23
|
+
case 'project': return 'fact';
|
|
24
|
+
case 'reference': return 'fact';
|
|
25
|
+
default: return 'observation';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const unquote = (v) => String(v ?? '').trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
30
|
+
|
|
31
|
+
/** -> { name, description, type, body } from a Claude memory file (frontmatter optional). */
|
|
32
|
+
export function parseMemoryFile(text, fallbackName = '') {
|
|
33
|
+
const src = String(text ?? '').replace(/\r\n/g, '\n');
|
|
34
|
+
let front = '';
|
|
35
|
+
let body = src;
|
|
36
|
+
const m = src.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
37
|
+
if (m) { front = m[1]; body = src.slice(m[0].length); }
|
|
38
|
+
const out = { name: '', description: '', type: '' };
|
|
39
|
+
let inMeta = false;
|
|
40
|
+
for (const raw of front.split('\n')) {
|
|
41
|
+
const top = raw.match(/^([A-Za-z_]+):\s*(.*)$/);
|
|
42
|
+
if (top) {
|
|
43
|
+
inMeta = top[1] === 'metadata';
|
|
44
|
+
if (top[1] === 'name') out.name = unquote(top[2]);
|
|
45
|
+
else if (top[1] === 'description') out.description = unquote(top[2]);
|
|
46
|
+
else if (top[1] === 'type') out.type = unquote(top[2]);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const nested = raw.match(/^\s+type:\s*(.*)$/);
|
|
50
|
+
if (inMeta && nested) out.type = unquote(nested[1]);
|
|
51
|
+
}
|
|
52
|
+
return { name: out.name || fallbackName, description: out.description, type: out.type, body: body.trim() };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Stable key: claude-memory:<project slug>/<file>. */
|
|
56
|
+
export function sourceKeyFor(file) {
|
|
57
|
+
const abs = resolve(file);
|
|
58
|
+
const parts = abs.split('/');
|
|
59
|
+
const mi = parts.lastIndexOf('memory');
|
|
60
|
+
const slug = mi > 0 ? parts[mi - 1] : createHash('sha256').update(dirname(abs)).digest('hex').slice(0, 8);
|
|
61
|
+
const rel = mi >= 0 ? parts.slice(mi + 1).join('/') : basename(abs);
|
|
62
|
+
return `claude-memory:${slug}/${rel}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Default memory dir for a cwd: ~/.claude/projects/<cwd with non-alnum -> '-'>/memory. */
|
|
66
|
+
export function defaultMemoryDir(cwd = process.cwd()) {
|
|
67
|
+
return join(homedir(), '.claude', 'projects', cwd.replace(/[^A-Za-z0-9]/g, '-'), 'memory');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Expand dirs to their *.md files; MEMORY.md (the index) is skipped. */
|
|
71
|
+
export function expandPaths(paths) {
|
|
72
|
+
const files = [];
|
|
73
|
+
for (const p of paths) {
|
|
74
|
+
const abs = resolve(p);
|
|
75
|
+
if (!existsSync(abs)) { files.push({ file: abs, missing: true }); continue; }
|
|
76
|
+
if (statSync(abs).isDirectory()) {
|
|
77
|
+
for (const f of readdirSync(abs).sort()) if (f.endsWith('.md')) files.push({ file: join(abs, f) });
|
|
78
|
+
} else files.push({ file: abs });
|
|
79
|
+
}
|
|
80
|
+
return files.filter((f) => f.missing || basename(f.file) !== 'MEMORY.md');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const statePathDefault = () => join(homedir(), '.quox', 'memory-sync-state.json');
|
|
84
|
+
function loadState(p) { try { return JSON.parse(readFileSync(p, 'utf8')) || {}; } catch { return {}; } }
|
|
85
|
+
function saveState(p, state) {
|
|
86
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
87
|
+
const tmp = `${p}.${process.pid}.tmp`;
|
|
88
|
+
writeFileSync(tmp, JSON.stringify(state, null, 2));
|
|
89
|
+
renameSync(tmp, p);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Sync files into Quox memory. client = the CLI http client; post/patch/get go to
|
|
94
|
+
* the memory service paths. Returns [{ file, key, name, action, id?, kinds?, error? }]
|
|
95
|
+
* where action is created | updated | unchanged | refused | missing | error | would-create | would-update.
|
|
96
|
+
*/
|
|
97
|
+
export async function syncMemoryFiles({ client, orgId = 'default', files, dryRun = false, statePath = statePathDefault(), memoryEp }) {
|
|
98
|
+
const state = loadState(statePath);
|
|
99
|
+
const mine = (state[orgId] ||= {});
|
|
100
|
+
let remote; // lazily loaded recovery index: source_key -> {id, hash}
|
|
101
|
+
const recover = async (key) => {
|
|
102
|
+
if (!remote) {
|
|
103
|
+
remote = {};
|
|
104
|
+
try {
|
|
105
|
+
const data = await client.get(memoryEp.path, { service: memoryEp.service, query: { limit: 1000 } });
|
|
106
|
+
for (const m of (data?.memories || [])) {
|
|
107
|
+
const sk = m?.metadata?.source_key;
|
|
108
|
+
if (sk) remote[sk] = { id: m.id, hash: m.metadata.content_hash };
|
|
109
|
+
}
|
|
110
|
+
} catch { /* recovery is best effort; worst case a first-time key creates */ }
|
|
111
|
+
}
|
|
112
|
+
return remote[key];
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const results = [];
|
|
116
|
+
for (const entry of files) {
|
|
117
|
+
const { file } = entry;
|
|
118
|
+
const key = sourceKeyFor(file);
|
|
119
|
+
if (entry.missing) { results.push({ file, key, name: basename(file), action: 'missing' }); continue; }
|
|
120
|
+
let text;
|
|
121
|
+
try { text = readFileSync(file, 'utf8'); } catch (e) { results.push({ file, key, name: basename(file), action: 'error', error: e.message }); continue; }
|
|
122
|
+
const parsed = parseMemoryFile(text, basename(file, '.md'));
|
|
123
|
+
const kinds = secretKinds(text, { precise: true });
|
|
124
|
+
if (kinds.length) { results.push({ file, key, name: parsed.name, action: 'refused', rule: kinds[0], kinds }); continue; }
|
|
125
|
+
let content = [parsed.name, parsed.description].filter(Boolean).join(': ');
|
|
126
|
+
content = (content ? content + '\n\n' : '') + parsed.body;
|
|
127
|
+
content = content.slice(0, MAX_CONTENT);
|
|
128
|
+
const hash = createHash('sha256').update(content + '\0' + parsed.type).digest('hex');
|
|
129
|
+
const known = mine[key] || await recover(key);
|
|
130
|
+
if (known && known.hash === hash) { mine[key] = { id: known.id, hash }; results.push({ file, key, name: parsed.name, action: 'unchanged', id: known.id }); continue; }
|
|
131
|
+
const metadata = { source: 'claude-memory', source_key: key, content_hash: hash, source_file: basename(file), claude_type: parsed.type || null };
|
|
132
|
+
const tags = ['claude-memory', ...(parsed.type ? [`claude-memory:${parsed.type}`] : [])];
|
|
133
|
+
if (dryRun) { results.push({ file, key, name: parsed.name, action: known ? 'would-update' : 'would-create', id: known?.id }); continue; }
|
|
134
|
+
try {
|
|
135
|
+
let id = known?.id;
|
|
136
|
+
let action = 'created';
|
|
137
|
+
if (id) {
|
|
138
|
+
try {
|
|
139
|
+
await client.patch(`${memoryEp.path}/${id}`, { service: memoryEp.service, body: { content, tags, metadata } });
|
|
140
|
+
action = 'updated';
|
|
141
|
+
} catch (e) {
|
|
142
|
+
if (e?.status !== 404) throw e;
|
|
143
|
+
id = null; // the Quox memory was removed; make a fresh one
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (!id) {
|
|
147
|
+
const created = await client.post(memoryEp.path, { service: memoryEp.service, body: { type: quoxTypeFor(parsed.type), content, tags, metadata } });
|
|
148
|
+
id = created?.id || created?.memory?.id || null;
|
|
149
|
+
}
|
|
150
|
+
if (id) mine[key] = { id, hash };
|
|
151
|
+
results.push({ file, key, name: parsed.name, action, id });
|
|
152
|
+
} catch (e) {
|
|
153
|
+
results.push({ file, key, name: parsed.name, action: 'error', error: e?.message || String(e) });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (!dryRun) { try { saveState(statePath, state); } catch { /* state is a cache; recovery covers it */ } }
|
|
157
|
+
return results;
|
|
158
|
+
}
|