memorix 1.2.1 → 1.2.3
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 +23 -0
- package/README.md +14 -2
- package/README.zh-CN.md +14 -2
- package/dist/cli/index.js +15424 -13780
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +1337 -536
- package/dist/index.js.map +1 -1
- package/dist/maintenance-runner.d.ts +1 -1
- package/dist/maintenance-runner.js +8458 -8087
- package/dist/maintenance-runner.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +23 -0
- package/dist/sdk.d.ts +7 -2
- package/dist/sdk.js +1365 -542
- package/dist/sdk.js.map +1 -1
- package/dist/types.d.ts +49 -1
- package/dist/types.js.map +1 -1
- package/docs/1.2.2-MEMORY-CONTROL-PLANE.md +434 -0
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +4 -0
- package/docs/API_REFERENCE.md +24 -4
- package/docs/README.md +1 -1
- package/docs/dev-log/progress.txt +101 -11
- package/package.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/src/cli/command-guide.ts +192 -0
- package/src/cli/commands/audit.ts +9 -4
- package/src/cli/commands/cleanup.ts +5 -1
- package/src/cli/commands/codegraph.ts +15 -5
- package/src/cli/commands/context.ts +3 -2
- package/src/cli/commands/doctor.ts +4 -2
- package/src/cli/commands/explain.ts +9 -3
- package/src/cli/commands/handoff.ts +21 -7
- package/src/cli/commands/identity.ts +116 -0
- package/src/cli/commands/ingest-image.ts +5 -3
- package/src/cli/commands/lock.ts +11 -10
- package/src/cli/commands/memory.ts +58 -21
- package/src/cli/commands/message.ts +19 -14
- package/src/cli/commands/operator-shared.ts +98 -3
- package/src/cli/commands/poll.ts +16 -6
- package/src/cli/commands/reasoning.ts +17 -3
- package/src/cli/commands/retention.ts +9 -4
- package/src/cli/commands/serve-http.ts +8 -2
- package/src/cli/commands/session.ts +44 -10
- package/src/cli/commands/skills.ts +10 -5
- package/src/cli/commands/status.ts +4 -3
- package/src/cli/commands/task.ts +26 -17
- package/src/cli/commands/team.ts +14 -10
- package/src/cli/commands/transfer.ts +63 -10
- package/src/cli/identity.ts +89 -0
- package/src/cli/index.ts +96 -19
- package/src/cli/invocation.ts +115 -0
- package/src/cli/tui/chat-service.ts +41 -18
- package/src/cli/tui/data.ts +23 -44
- package/src/cli/tui/operator-context.ts +60 -0
- package/src/cli/tui/session-service.ts +3 -2
- package/src/cli/tui/views/MemoryView.tsx +10 -8
- package/src/codegraph/auto-context.ts +31 -2
- package/src/codegraph/context-pack.ts +1 -0
- package/src/codegraph/project-context.ts +2 -0
- package/src/compact/engine.ts +26 -10
- package/src/compact/index-format.ts +25 -2
- package/src/dashboard/server.ts +46 -9
- package/src/hooks/admission.ts +117 -0
- package/src/hooks/handler.ts +98 -91
- package/src/knowledge/context-assembly.ts +97 -0
- package/src/knowledge/workset.ts +179 -10
- package/src/memory/admission.ts +57 -0
- package/src/memory/consolidation.ts +13 -2
- package/src/memory/disclosure-policy.ts +6 -1
- package/src/memory/export-import.ts +11 -3
- package/src/memory/graph-context.ts +8 -2
- package/src/memory/observations.ts +162 -4
- package/src/memory/quality-audit.ts +2 -0
- package/src/memory/retention.ts +22 -2
- package/src/memory/session.ts +29 -11
- package/src/memory/visibility.ts +80 -0
- package/src/orchestrate/memorix-bridge.ts +38 -0
- package/src/runtime/control-plane-maintenance.ts +1 -0
- package/src/runtime/isolated-maintenance.ts +1 -0
- package/src/runtime/lifecycle.ts +18 -0
- package/src/runtime/maintenance-jobs.ts +1 -0
- package/src/runtime/maintenance-runner.ts +2 -0
- package/src/runtime/project-maintenance.ts +89 -0
- package/src/sdk.ts +35 -5
- package/src/server.ts +267 -83
- package/src/store/orama-store.ts +61 -6
- package/src/store/sqlite-db.ts +23 -1
- package/src/store/sqlite-store.ts +12 -2
- package/src/team/handoff.ts +7 -0
- package/src/types.ts +51 -0
- package/src/wiki/generator.ts +2 -0
|
@@ -1,7 +1,41 @@
|
|
|
1
1
|
import { defineCommand } from 'citty';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import { exportAsJson, exportAsMarkdown, importFromJson } from '../../memory/export-import.js';
|
|
3
5
|
import { emitError, emitResult, getCliProjectContext } from './operator-shared.js';
|
|
4
6
|
|
|
7
|
+
async function readStandardInput(): Promise<string> {
|
|
8
|
+
return new Promise((resolve, reject) => {
|
|
9
|
+
let content = '';
|
|
10
|
+
process.stdin.setEncoding('utf8');
|
|
11
|
+
process.stdin.on('data', (chunk) => {
|
|
12
|
+
content += chunk;
|
|
13
|
+
});
|
|
14
|
+
process.stdin.on('end', () => resolve(content));
|
|
15
|
+
process.stdin.on('error', reject);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readImportPayload(args: Record<string, unknown>, projectRoot: string): Promise<string> {
|
|
20
|
+
const data = typeof args.data === 'string' ? args.data.trim() : '';
|
|
21
|
+
const file = typeof args.file === 'string' ? args.file.trim() : '';
|
|
22
|
+
const readFromStdin = args.stdin === true || data === '-';
|
|
23
|
+
const sources = Number(Boolean(data && data !== '-')) + Number(Boolean(file)) + Number(readFromStdin);
|
|
24
|
+
if (sources !== 1) {
|
|
25
|
+
throw new Error('Choose exactly one import source: --data <json>, --file <path>, or --stdin.');
|
|
26
|
+
}
|
|
27
|
+
if (readFromStdin) return readStandardInput();
|
|
28
|
+
if (file) return fs.readFile(path.resolve(projectRoot, file), 'utf8');
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function writeExportPayload(projectRoot: string, output: string, payload: string): Promise<string> {
|
|
33
|
+
const outputPath = path.resolve(projectRoot, output);
|
|
34
|
+
await fs.mkdir(path.dirname(outputPath), { recursive: true });
|
|
35
|
+
await fs.writeFile(outputPath, payload, 'utf8');
|
|
36
|
+
return outputPath;
|
|
37
|
+
}
|
|
38
|
+
|
|
5
39
|
export default defineCommand({
|
|
6
40
|
meta: {
|
|
7
41
|
name: 'transfer',
|
|
@@ -10,6 +44,9 @@ export default defineCommand({
|
|
|
10
44
|
args: {
|
|
11
45
|
format: { type: 'string', description: 'Export format: json or markdown' },
|
|
12
46
|
data: { type: 'string', description: 'JSON payload from a previous export' },
|
|
47
|
+
file: { type: 'string', description: 'File containing a JSON payload to import' },
|
|
48
|
+
stdin: { type: 'boolean', description: 'Read a JSON import payload from standard input' },
|
|
49
|
+
out: { type: 'string', description: 'Write the export payload to this file instead of stdout' },
|
|
13
50
|
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
14
51
|
},
|
|
15
52
|
run: async ({ args }) => {
|
|
@@ -17,31 +54,45 @@ export default defineCommand({
|
|
|
17
54
|
const asJson = !!args.json;
|
|
18
55
|
|
|
19
56
|
try {
|
|
20
|
-
const { project, dataDir } = await getCliProjectContext();
|
|
57
|
+
const { project, dataDir, reader } = await getCliProjectContext();
|
|
21
58
|
|
|
22
59
|
switch (action) {
|
|
23
60
|
case 'export': {
|
|
24
61
|
const format = (args.format as string | undefined) === 'markdown' ? 'markdown' : 'json';
|
|
25
62
|
if (format === 'markdown') {
|
|
26
|
-
const markdown = await exportAsMarkdown(dataDir, project.id);
|
|
63
|
+
const markdown = await exportAsMarkdown(dataDir, project.id, reader);
|
|
64
|
+
const output = (args.out as string | undefined)?.trim();
|
|
65
|
+
if (output) {
|
|
66
|
+
const outputPath = await writeExportPayload(project.rootPath, output, markdown);
|
|
67
|
+
emitResult({ project, format, outputPath }, `Export written: ${outputPath}`, asJson);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
27
70
|
emitResult({ project, format, markdown }, markdown, asJson);
|
|
28
71
|
return;
|
|
29
72
|
}
|
|
30
|
-
const exported = await exportAsJson(dataDir, project.id);
|
|
73
|
+
const exported = await exportAsJson(dataDir, project.id, reader);
|
|
74
|
+
const serialized = JSON.stringify(exported, null, 2);
|
|
75
|
+
const output = (args.out as string | undefined)?.trim();
|
|
76
|
+
if (output) {
|
|
77
|
+
const outputPath = await writeExportPayload(project.rootPath, output, `${serialized}\n`);
|
|
78
|
+
emitResult(
|
|
79
|
+
{ project, format, outputPath, stats: exported.stats },
|
|
80
|
+
`Export written: ${outputPath}`,
|
|
81
|
+
asJson,
|
|
82
|
+
);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
31
85
|
emitResult(
|
|
32
86
|
{ project, format, export: exported },
|
|
33
|
-
|
|
87
|
+
serialized,
|
|
34
88
|
asJson,
|
|
35
89
|
);
|
|
36
90
|
return;
|
|
37
91
|
}
|
|
38
92
|
|
|
39
93
|
case 'import': {
|
|
40
|
-
const raw = (args
|
|
41
|
-
if (!raw)
|
|
42
|
-
emitError('data is required for "memorix transfer import"', asJson);
|
|
43
|
-
return;
|
|
44
|
-
}
|
|
94
|
+
const raw = (await readImportPayload(args as Record<string, unknown>, project.rootPath)).trim();
|
|
95
|
+
if (!raw) throw new Error('Import payload is empty.');
|
|
45
96
|
let parsed: Parameters<typeof importFromJson>[1];
|
|
46
97
|
try {
|
|
47
98
|
parsed = JSON.parse(raw);
|
|
@@ -62,7 +113,9 @@ export default defineCommand({
|
|
|
62
113
|
console.log('Memorix Transfer Commands');
|
|
63
114
|
console.log('');
|
|
64
115
|
console.log('Usage:');
|
|
65
|
-
console.log(' memorix transfer export [--format json|markdown]');
|
|
116
|
+
console.log(' memorix transfer export [--format json|markdown] [--out ./.memorix-export.json]');
|
|
117
|
+
console.log(' memorix transfer import --file ./.memorix-export.json');
|
|
118
|
+
console.log(' memorix transfer import --stdin');
|
|
66
119
|
console.log(' memorix transfer import --data "<json>"');
|
|
67
120
|
}
|
|
68
121
|
} catch (error) {
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { ObservationReader, ProjectInfo } from '../types.js';
|
|
4
|
+
import type { TeamStore } from '../team/team-store.js';
|
|
5
|
+
|
|
6
|
+
export interface CliIdentity {
|
|
7
|
+
agentId: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
activatedAt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface CliIdentityResolution {
|
|
13
|
+
identity: CliIdentity | null;
|
|
14
|
+
reader: ObservationReader;
|
|
15
|
+
warning?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function identityPath(dataDir: string): string {
|
|
19
|
+
return path.join(dataDir, 'cli-identity.json');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function isCliIdentity(value: unknown): value is CliIdentity {
|
|
23
|
+
if (!value || typeof value !== 'object') return false;
|
|
24
|
+
const candidate = value as Partial<CliIdentity>;
|
|
25
|
+
return typeof candidate.agentId === 'string'
|
|
26
|
+
&& typeof candidate.projectId === 'string'
|
|
27
|
+
&& typeof candidate.activatedAt === 'string';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function loadCliIdentity(dataDir: string, projectId: string): Promise<CliIdentity | null> {
|
|
31
|
+
try {
|
|
32
|
+
const parsed = JSON.parse(await fs.readFile(identityPath(dataDir), 'utf8'));
|
|
33
|
+
return isCliIdentity(parsed) && parsed.projectId === projectId ? parsed : null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function saveCliIdentity(dataDir: string, identity: CliIdentity): Promise<void> {
|
|
40
|
+
await fs.writeFile(identityPath(dataDir), `${JSON.stringify(identity, null, 2)}\n`, 'utf8');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function clearCliIdentity(dataDir: string): Promise<void> {
|
|
44
|
+
await fs.rm(identityPath(dataDir), { force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* An unbound terminal remains project-scoped. Private and team access becomes
|
|
49
|
+
* available only after the operator explicitly chooses an active team identity.
|
|
50
|
+
*/
|
|
51
|
+
export async function resolveCliIdentity(options: {
|
|
52
|
+
project: ProjectInfo;
|
|
53
|
+
dataDir: string;
|
|
54
|
+
teamStore: TeamStore;
|
|
55
|
+
explicitActorId?: string;
|
|
56
|
+
}): Promise<CliIdentityResolution> {
|
|
57
|
+
const stored = await loadCliIdentity(options.dataDir, options.project.id);
|
|
58
|
+
const requestedAgentId = options.explicitActorId ?? stored?.agentId;
|
|
59
|
+
const projectReader: ObservationReader = { projectId: options.project.id };
|
|
60
|
+
if (!requestedAgentId) return { identity: null, reader: projectReader };
|
|
61
|
+
|
|
62
|
+
const agent = options.teamStore.getAgent(requestedAgentId);
|
|
63
|
+
const activeForProject = agent
|
|
64
|
+
&& agent.project_id === options.project.id
|
|
65
|
+
&& agent.status === 'active';
|
|
66
|
+
if (!activeForProject) {
|
|
67
|
+
if (options.explicitActorId) {
|
|
68
|
+
throw new Error(`CLI actor "${requestedAgentId}" is not an active member of this project.`);
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
identity: null,
|
|
72
|
+
reader: projectReader,
|
|
73
|
+
warning: `Saved CLI identity "${requestedAgentId}" is no longer active for this project.`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
identity: stored ?? {
|
|
79
|
+
agentId: agent.agent_id,
|
|
80
|
+
projectId: options.project.id,
|
|
81
|
+
activatedAt: new Date().toISOString(),
|
|
82
|
+
},
|
|
83
|
+
reader: {
|
|
84
|
+
projectId: options.project.id,
|
|
85
|
+
agentId: agent.agent_id,
|
|
86
|
+
isTeamMember: true,
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
package/src/cli/index.ts
CHANGED
|
@@ -19,6 +19,8 @@ import { execSync, spawn } from 'node:child_process';
|
|
|
19
19
|
import { getCliVersion } from './version.js';
|
|
20
20
|
import { importBundledMemcode } from './memcode-bootstrap.js';
|
|
21
21
|
import { installCliPipeErrorGuard } from './pipe-errors.js';
|
|
22
|
+
import { normalizeCliInvocation } from './invocation.js';
|
|
23
|
+
import { printCliGuideForHelp, renderCliGuide } from './command-guide.js';
|
|
22
24
|
|
|
23
25
|
installCliPipeErrorGuard();
|
|
24
26
|
|
|
@@ -129,8 +131,9 @@ async function getWorkbenchHeader(): Promise<string[]> {
|
|
|
129
131
|
const dataDir = await getProjectDataDir(proj.id);
|
|
130
132
|
await initStore(dataDir);
|
|
131
133
|
const { getObservationStore: getStore } = await import('../store/obs-store.js');
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
+
const { filterReadableObservations } = await import('../memory/visibility.js');
|
|
135
|
+
const obs = filterReadableObservations(await getStore().loadAll(), { projectId: proj.id });
|
|
136
|
+
const active = obs.filter((o) => (o.status ?? 'active') === 'active').length;
|
|
134
137
|
if (active > 0) {
|
|
135
138
|
memLabel = `${BOLD}${active}${RESET} ${DIM}active${RESET}`;
|
|
136
139
|
}
|
|
@@ -801,6 +804,7 @@ async function runSearch(query: string): Promise<void> {
|
|
|
801
804
|
|
|
802
805
|
try {
|
|
803
806
|
const { searchObservations, getDb, hydrateIndex } = await import('../store/orama-store.js');
|
|
807
|
+
const { filterReadableObservations } = await import('../memory/visibility.js');
|
|
804
808
|
const { getProjectDataDir } = await import('../store/persistence.js');
|
|
805
809
|
const { detectProject } = await import('../project/detector.js');
|
|
806
810
|
const { initObservations } = await import('../memory/observations.js');
|
|
@@ -825,7 +829,7 @@ async function runSearch(query: string): Promise<void> {
|
|
|
825
829
|
await hydrateIndex(allObs);
|
|
826
830
|
mark('hydrateIndex');
|
|
827
831
|
|
|
828
|
-
const results = await searchObservations({ query, limit: 10, projectId: project.id });
|
|
832
|
+
const results = await searchObservations({ query, limit: 10, projectId: project.id, reader: { projectId: project.id } });
|
|
829
833
|
mark('search');
|
|
830
834
|
s.stop('Search complete');
|
|
831
835
|
|
|
@@ -855,14 +859,16 @@ async function runList(): Promise<void> {
|
|
|
855
859
|
const { getProjectDataDir } = await import('../store/persistence.js');
|
|
856
860
|
const { detectProject } = await import('../project/detector.js');
|
|
857
861
|
const { initObservationStore: initStore, getObservationStore: getStore } = await import('../store/obs-store.js');
|
|
862
|
+
const { filterReadableObservations } = await import('../memory/visibility.js');
|
|
858
863
|
|
|
859
864
|
const project = detectProject(process.cwd());
|
|
860
865
|
if (!project) { s.stop('No git repo'); p.log.error(NO_GIT_MSG); return; }
|
|
861
866
|
const dataDir = await getProjectDataDir(project.id);
|
|
862
867
|
await initStore(dataDir);
|
|
863
|
-
const observations =
|
|
864
|
-
|
|
865
|
-
|
|
868
|
+
const observations = filterReadableObservations(
|
|
869
|
+
await getStore().loadAll(),
|
|
870
|
+
{ projectId: project.id },
|
|
871
|
+
);
|
|
866
872
|
|
|
867
873
|
const active = observations.filter(o => (o.status ?? 'active') === 'active');
|
|
868
874
|
const recent = active.slice(-10).reverse();
|
|
@@ -876,7 +882,7 @@ async function runList(): Promise<void> {
|
|
|
876
882
|
|
|
877
883
|
console.log('');
|
|
878
884
|
for (const o of recent) {
|
|
879
|
-
const typeLabel = { gotcha: '[!]', decision: '[D]', 'problem-solution': '[S]', discovery: '[?]', 'how-it-works': '[H]', 'what-changed': '[C]' }[o.type] ?? '[·]';
|
|
885
|
+
const typeLabel = ({ gotcha: '[!]', decision: '[D]', 'problem-solution': '[S]', discovery: '[?]', 'how-it-works': '[H]', 'what-changed': '[C]' } as Record<string, string>)[o.type] ?? '[·]';
|
|
880
886
|
console.log(` ${typeLabel} #${o.id} ${o.title?.slice(0, 60) ?? '(untitled)'}`);
|
|
881
887
|
}
|
|
882
888
|
console.log('');
|
|
@@ -927,11 +933,26 @@ async function runCommand(cmd: string, _args: string[] = []): Promise<void> {
|
|
|
927
933
|
// Main command
|
|
928
934
|
// ============================================================
|
|
929
935
|
|
|
936
|
+
async function runMemoryShortcut(action: string, args: Record<string, unknown>): Promise<void> {
|
|
937
|
+
const { detectProject } = await import('../project/detector.js');
|
|
938
|
+
if (!detectProject(process.cwd())) {
|
|
939
|
+
console.log(NO_GIT_MSG);
|
|
940
|
+
process.exitCode = 1;
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
const memory = await import('./commands/memory.js');
|
|
944
|
+
await memory.default.run?.({
|
|
945
|
+
args: { ...args, _: [action] },
|
|
946
|
+
rawArgs: [],
|
|
947
|
+
cmd: memory.default,
|
|
948
|
+
} as any);
|
|
949
|
+
}
|
|
950
|
+
|
|
930
951
|
const main = defineCommand({
|
|
931
952
|
meta: {
|
|
932
953
|
name: 'memorix',
|
|
933
954
|
version: getCliVersion(),
|
|
934
|
-
description: 'Local-first memory control plane for AI coding agents
|
|
955
|
+
description: 'Local-first memory control plane for AI coding agents through CLI, MCP, and local workflows',
|
|
935
956
|
},
|
|
936
957
|
subCommands: {
|
|
937
958
|
// One-shot product commands (primary user paths)
|
|
@@ -960,18 +981,55 @@ const main = defineCommand({
|
|
|
960
981
|
},
|
|
961
982
|
})),
|
|
962
983
|
search: () => Promise.resolve(defineCommand({
|
|
963
|
-
meta: { name: 'search', description: '
|
|
964
|
-
args: {
|
|
965
|
-
|
|
984
|
+
meta: { name: 'search', description: 'Shortcut for `memorix memory search`' },
|
|
985
|
+
args: {
|
|
986
|
+
query: { type: 'positional', description: 'Search query', required: true },
|
|
987
|
+
limit: { type: 'string', description: 'Maximum results' },
|
|
988
|
+
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
989
|
+
},
|
|
990
|
+
async run({ args }) {
|
|
991
|
+
await runMemoryShortcut('search', {
|
|
992
|
+
query: args.query,
|
|
993
|
+
limit: args.limit,
|
|
994
|
+
json: args.json,
|
|
995
|
+
});
|
|
996
|
+
},
|
|
966
997
|
})),
|
|
967
998
|
remember: () => Promise.resolve(defineCommand({
|
|
968
|
-
meta: { name: 'remember', description: '
|
|
969
|
-
args: {
|
|
970
|
-
|
|
999
|
+
meta: { name: 'remember', description: 'Shortcut for `memorix memory store`' },
|
|
1000
|
+
args: {
|
|
1001
|
+
text: { type: 'positional', description: 'Text to remember', required: true },
|
|
1002
|
+
title: { type: 'string', description: 'Optional observation title' },
|
|
1003
|
+
type: { type: 'string', description: 'Observation type' },
|
|
1004
|
+
visibility: { type: 'string', description: 'project (default), personal, or team' },
|
|
1005
|
+
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
1006
|
+
},
|
|
1007
|
+
async run({ args }) {
|
|
1008
|
+
await runMemoryShortcut('store', {
|
|
1009
|
+
text: args.text,
|
|
1010
|
+
title: args.title,
|
|
1011
|
+
type: args.type,
|
|
1012
|
+
visibility: args.visibility,
|
|
1013
|
+
json: args.json,
|
|
1014
|
+
});
|
|
1015
|
+
},
|
|
971
1016
|
})),
|
|
972
1017
|
recent: () => Promise.resolve(defineCommand({
|
|
973
|
-
meta: { name: 'recent', description: '
|
|
974
|
-
|
|
1018
|
+
meta: { name: 'recent', description: 'Shortcut for `memorix memory recent`' },
|
|
1019
|
+
args: {
|
|
1020
|
+
limit: { type: 'string', description: 'Maximum results' },
|
|
1021
|
+
json: { type: 'boolean', description: 'Emit machine-readable JSON output' },
|
|
1022
|
+
},
|
|
1023
|
+
async run({ args }) { await runMemoryShortcut('recent', { limit: args.limit, json: args.json }); },
|
|
1024
|
+
})),
|
|
1025
|
+
help: () => Promise.resolve(defineCommand({
|
|
1026
|
+
meta: { name: 'help', description: 'Show action-oriented help for a command group' },
|
|
1027
|
+
args: {
|
|
1028
|
+
command: { type: 'positional', description: 'Command group to inspect', required: false },
|
|
1029
|
+
},
|
|
1030
|
+
async run({ args }) {
|
|
1031
|
+
console.log(renderCliGuide(args.command as string | undefined));
|
|
1032
|
+
},
|
|
975
1033
|
})),
|
|
976
1034
|
// Infrastructure commands
|
|
977
1035
|
init: () => import('./commands/init.js').then(m => m.default),
|
|
@@ -996,6 +1054,7 @@ const main = defineCommand({
|
|
|
996
1054
|
audit: () => import('./commands/audit.js').then(m => m.default),
|
|
997
1055
|
transfer: () => import('./commands/transfer.js').then(m => m.default),
|
|
998
1056
|
skills: () => import('./commands/skills.js').then(m => m.default),
|
|
1057
|
+
identity: () => import('./commands/identity.js').then(m => m.default),
|
|
999
1058
|
session: () => import('./commands/session.js').then(m => m.default),
|
|
1000
1059
|
team: () => import('./commands/team.js').then(m => m.default),
|
|
1001
1060
|
task: () => import('./commands/task.js').then(m => m.default),
|
|
@@ -1031,6 +1090,13 @@ const main = defineCommand({
|
|
|
1031
1090
|
cleanup: () => import('./commands/cleanup.js').then(m => m.default),
|
|
1032
1091
|
uninstall: () => import('./commands/uninstall.js').then(m => m.default),
|
|
1033
1092
|
orchestrate: () => import('./commands/orchestrate.js').then(m => m.default),
|
|
1093
|
+
workbench: () => Promise.resolve(defineCommand({
|
|
1094
|
+
meta: { name: 'workbench', description: 'Open the interactive terminal memory control plane' },
|
|
1095
|
+
async run() {
|
|
1096
|
+
const { startWorkbench } = await import('./workbench.js');
|
|
1097
|
+
await startWorkbench();
|
|
1098
|
+
},
|
|
1099
|
+
})),
|
|
1034
1100
|
memcode: () => Promise.resolve(defineCommand({
|
|
1035
1101
|
meta: { name: 'memcode', description: 'Enter memcode TUI — native coding agent with memory' },
|
|
1036
1102
|
async run() {
|
|
@@ -1048,8 +1114,8 @@ const main = defineCommand({
|
|
|
1048
1114
|
// Guard: if citty already resolved a subcommand, its run() was called before this.
|
|
1049
1115
|
// Detect by checking if the first CLI arg matches a registered subcommand name.
|
|
1050
1116
|
const firstArg = process.argv[2];
|
|
1051
|
-
const knownSubs = ['ask', 'search', 'remember', 'recent', 'memcode', 'config',
|
|
1052
|
-
'init', 'setup', 'integrate', 'memory', 'context', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills',
|
|
1117
|
+
const knownSubs = ['ask', 'search', 'remember', 'recent', 'help', 'workbench', 'memcode', 'config',
|
|
1118
|
+
'init', 'setup', 'integrate', 'memory', 'context', 'explain', 'codegraph', 'knowledge', 'reasoning', 'retention', 'formation', 'audit', 'transfer', 'skills', 'identity',
|
|
1053
1119
|
'session', 'team', 'task', 'message', 'lock', 'handoff', 'poll',
|
|
1054
1120
|
'receipt',
|
|
1055
1121
|
'serve', 'serve-http', 'status', 'sync',
|
|
@@ -1080,6 +1146,8 @@ const main = defineCommand({
|
|
|
1080
1146
|
console.error(`Memorix v${getCliVersion()} — Local-first memory control plane\n`);
|
|
1081
1147
|
console.error('Usage: memorix <command>\n');
|
|
1082
1148
|
console.error('Commands:');
|
|
1149
|
+
console.error(' help Show action-oriented help (`memorix memory --help`)');
|
|
1150
|
+
console.error(' workbench Open interactive terminal memory control plane');
|
|
1083
1151
|
console.error(' memcode Enter memcode TUI (native coding agent)');
|
|
1084
1152
|
console.error(' ask "q" Ask Memorix a question (single-shot chat)');
|
|
1085
1153
|
console.error(' Pipe: echo "q" | memorix ask');
|
|
@@ -1096,6 +1164,7 @@ const main = defineCommand({
|
|
|
1096
1164
|
console.error(' audit Audit trail and project attribution checks');
|
|
1097
1165
|
console.error(' transfer Export/import memory snapshots');
|
|
1098
1166
|
console.error(' skills List/generate/show project skills');
|
|
1167
|
+
console.error(' identity Select the explicit CLI actor for private/team memory');
|
|
1099
1168
|
console.error(' team Join/status/role operations for coordination state');
|
|
1100
1169
|
console.error(' task Create/claim/complete/list team tasks');
|
|
1101
1170
|
console.error(' message Send/broadcast/read team messages');
|
|
@@ -1121,4 +1190,12 @@ const main = defineCommand({
|
|
|
1121
1190
|
},
|
|
1122
1191
|
});
|
|
1123
1192
|
|
|
1124
|
-
|
|
1193
|
+
try {
|
|
1194
|
+
normalizeCliInvocation();
|
|
1195
|
+
if (!printCliGuideForHelp()) {
|
|
1196
|
+
runMain(main);
|
|
1197
|
+
}
|
|
1198
|
+
} catch (error) {
|
|
1199
|
+
console.error(`Memorix CLI invocation error: ${error instanceof Error ? error.message : String(error)}`);
|
|
1200
|
+
process.exitCode = 1;
|
|
1201
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, statSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
export interface CliInvocation {
|
|
5
|
+
projectRoot?: string;
|
|
6
|
+
actorId?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type InvocationOption = 'projectRoot' | 'actorId';
|
|
10
|
+
|
|
11
|
+
const GLOBAL_OPTIONS: Record<string, InvocationOption> = {
|
|
12
|
+
'--cwd': 'projectRoot',
|
|
13
|
+
'--project-root': 'projectRoot',
|
|
14
|
+
'--as': 'actorId',
|
|
15
|
+
'--actor': 'actorId',
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Existing commands use camelCase Citty keys. Preserve them for compatibility
|
|
19
|
+
// while accepting the shell-native kebab-case spelling everywhere.
|
|
20
|
+
const FLAG_ALIASES: Record<string, string> = {
|
|
21
|
+
'--agent-type': '--agentType',
|
|
22
|
+
'--instance-id': '--instanceId',
|
|
23
|
+
'--agent-id': '--agentId',
|
|
24
|
+
'--from-agent-id': '--fromAgentId',
|
|
25
|
+
'--to-agent-id': '--toAgentId',
|
|
26
|
+
'--join-team': '--joinTeam',
|
|
27
|
+
'--session-id': '--sessionId',
|
|
28
|
+
'--task-id': '--taskId',
|
|
29
|
+
'--topic-key': '--topicKey',
|
|
30
|
+
'--graph-limit': '--graphLimit',
|
|
31
|
+
'--graph-query': '--graphQuery',
|
|
32
|
+
'--skill-id': '--skillId',
|
|
33
|
+
'--required-role': '--requiredRole',
|
|
34
|
+
'--preferred-role': '--preferredRole',
|
|
35
|
+
'--to-role': '--toRole',
|
|
36
|
+
'--handoff-status': '--handoffStatus',
|
|
37
|
+
'--max-concurrent': '--maxConcurrent',
|
|
38
|
+
'--role-id': '--roleId',
|
|
39
|
+
'--preferred-agent-types': '--preferredAgentTypes',
|
|
40
|
+
'--files-modified': '--filesModified',
|
|
41
|
+
'--expected-outcome': '--expectedOutcome',
|
|
42
|
+
'--related-commits': '--relatedCommits',
|
|
43
|
+
'--related-entities': '--relatedEntities',
|
|
44
|
+
'--mime-type': '--mimeType',
|
|
45
|
+
'--mark-read': '--markRead',
|
|
46
|
+
'--mark-inbox-read': '--markInboxRead',
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function readOption(argument: string): { name: string; value?: string } {
|
|
50
|
+
const equals = argument.indexOf('=');
|
|
51
|
+
return equals === -1
|
|
52
|
+
? { name: argument }
|
|
53
|
+
: { name: argument.slice(0, equals), value: argument.slice(equals + 1) };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Normalize the small set of global operator flags before Citty resolves a
|
|
58
|
+
* command. This gives every CLI command the same project and actor anchors
|
|
59
|
+
* without duplicating those flags across dozens of command definitions.
|
|
60
|
+
*/
|
|
61
|
+
export function normalizeCliInvocation(argv: string[] = process.argv): CliInvocation {
|
|
62
|
+
const normalized = argv.slice(0, 2);
|
|
63
|
+
const invocation: CliInvocation = {};
|
|
64
|
+
|
|
65
|
+
for (let index = 2; index < argv.length; index += 1) {
|
|
66
|
+
const argument = argv[index];
|
|
67
|
+
if (argument === '--') {
|
|
68
|
+
normalized.push(...argv.slice(index));
|
|
69
|
+
break;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const { name, value: inlineValue } = readOption(argument);
|
|
73
|
+
const target = GLOBAL_OPTIONS[name];
|
|
74
|
+
if (!target) {
|
|
75
|
+
const alias = FLAG_ALIASES[name];
|
|
76
|
+
normalized.push(alias
|
|
77
|
+
? (inlineValue === undefined ? alias : `${alias}=${inlineValue}`)
|
|
78
|
+
: argument);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const value = inlineValue ?? argv[index + 1];
|
|
83
|
+
if (!value || value.startsWith('--')) {
|
|
84
|
+
throw new Error(`${name} requires a value`);
|
|
85
|
+
}
|
|
86
|
+
invocation[target] = value;
|
|
87
|
+
if (inlineValue === undefined) index += 1;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (invocation.projectRoot) {
|
|
91
|
+
const projectRoot = resolve(invocation.projectRoot);
|
|
92
|
+
if (!existsSync(projectRoot) || !statSync(projectRoot).isDirectory()) {
|
|
93
|
+
throw new Error(`CLI project directory does not exist: ${projectRoot}`);
|
|
94
|
+
}
|
|
95
|
+
process.chdir(projectRoot);
|
|
96
|
+
process.env.MEMORIX_CLI_PROJECT_ROOT = projectRoot;
|
|
97
|
+
invocation.projectRoot = projectRoot;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (invocation.actorId) {
|
|
101
|
+
process.env.MEMORIX_CLI_ACTOR_ID = invocation.actorId;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
process.argv = normalized;
|
|
105
|
+
return invocation;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function getCliInvocation(): CliInvocation {
|
|
109
|
+
const projectRoot = process.env.MEMORIX_CLI_PROJECT_ROOT?.trim();
|
|
110
|
+
const actorId = process.env.MEMORIX_CLI_ACTOR_ID?.trim();
|
|
111
|
+
return {
|
|
112
|
+
...(projectRoot ? { projectRoot } : {}),
|
|
113
|
+
...(actorId ? { actorId } : {}),
|
|
114
|
+
};
|
|
115
|
+
}
|