agent-remnote 0.0.1 → 0.1.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/cli.js +2 -0
- package/dist/apps/cli/src/adapters/mcp.js +1 -0
- package/dist/apps/cli/src/commands/_enqueue.js +138 -0
- package/dist/apps/cli/src/commands/_shared.js +57 -0
- package/dist/apps/cli/src/commands/_tool.js +28 -0
- package/dist/apps/cli/src/commands/apply.js +81 -0
- package/dist/apps/cli/src/commands/config/index.js +3 -0
- package/dist/apps/cli/src/commands/config/print.js +28 -0
- package/dist/apps/cli/src/commands/daily/index.js +4 -0
- package/dist/apps/cli/src/commands/daily/summary.js +25 -0
- package/dist/apps/cli/src/commands/daily/write.js +145 -0
- package/dist/apps/cli/src/commands/db/backups.js +23 -0
- package/dist/apps/cli/src/commands/db/index.js +4 -0
- package/dist/apps/cli/src/commands/db/recent.js +178 -0
- package/dist/apps/cli/src/commands/doctor.js +124 -0
- package/dist/apps/cli/src/commands/index.js +73 -0
- package/dist/apps/cli/src/commands/ops/index.js +4 -0
- package/dist/apps/cli/src/commands/ops/list.js +12 -0
- package/dist/apps/cli/src/commands/ops/schema.js +77 -0
- package/dist/apps/cli/src/commands/queue/enqueue.js +73 -0
- package/dist/apps/cli/src/commands/queue/index.js +5 -0
- package/dist/apps/cli/src/commands/queue/inspect.js +26 -0
- package/dist/apps/cli/src/commands/queue/stats.js +14 -0
- package/dist/apps/cli/src/commands/read/by-reference.js +35 -0
- package/dist/apps/cli/src/commands/read/connections.js +15 -0
- package/dist/apps/cli/src/commands/read/index.js +21 -0
- package/dist/apps/cli/src/commands/read/inspect.js +34 -0
- package/dist/apps/cli/src/commands/read/outline.js +59 -0
- package/dist/apps/cli/src/commands/read/query.js +95 -0
- package/dist/apps/cli/src/commands/read/references.js +41 -0
- package/dist/apps/cli/src/commands/read/resolve-ref.js +32 -0
- package/dist/apps/cli/src/commands/read/search.js +40 -0
- package/dist/apps/cli/src/commands/read/table.js +32 -0
- package/dist/apps/cli/src/commands/todos/index.js +3 -0
- package/dist/apps/cli/src/commands/todos/list.js +33 -0
- package/dist/apps/cli/src/commands/topic/index.js +3 -0
- package/dist/apps/cli/src/commands/topic/summary.js +44 -0
- package/dist/apps/cli/src/commands/wechat/index.js +3 -0
- package/dist/apps/cli/src/commands/wechat/outline.js +430 -0
- package/dist/apps/cli/src/commands/write/bullet.js +76 -0
- package/dist/apps/cli/src/commands/write/index.js +4 -0
- package/dist/apps/cli/src/commands/write/md.js +91 -0
- package/dist/apps/cli/src/commands/ws/_shared.js +129 -0
- package/dist/apps/cli/src/commands/ws/ensure.js +22 -0
- package/dist/apps/cli/src/commands/ws/health.js +15 -0
- package/dist/apps/cli/src/commands/ws/index.js +21 -0
- package/dist/apps/cli/src/commands/ws/logs.js +95 -0
- package/dist/apps/cli/src/commands/ws/restart.js +73 -0
- package/dist/apps/cli/src/commands/ws/serve.js +52 -0
- package/dist/apps/cli/src/commands/ws/start.js +70 -0
- package/dist/apps/cli/src/commands/ws/status.js +60 -0
- package/dist/apps/cli/src/commands/ws/stop.js +59 -0
- package/dist/apps/cli/src/commands/ws/trigger.js +20 -0
- package/dist/apps/cli/src/main.js +79 -0
- package/dist/apps/cli/src/services/AppConfig.js +3 -0
- package/dist/apps/cli/src/services/Config.js +91 -0
- package/dist/apps/cli/src/services/DaemonFiles.js +91 -0
- package/dist/apps/cli/src/services/Errors.js +49 -0
- package/dist/apps/cli/src/services/Output.js +16 -0
- package/dist/apps/cli/src/services/Payload.js +90 -0
- package/dist/apps/cli/src/services/Process.js +94 -0
- package/dist/apps/cli/src/services/Queue.js +120 -0
- package/dist/apps/cli/src/services/RefResolver.js +111 -0
- package/dist/apps/cli/src/services/RemDb.js +35 -0
- package/dist/apps/cli/src/services/WsClient.js +170 -0
- package/dist/apps/cli/tests/apply.contract.test.js +31 -0
- package/dist/apps/cli/tests/db-recent.contract.test.js +22 -0
- package/dist/apps/cli/tests/help.contract.test.js +30 -0
- package/dist/apps/cli/tests/helpers/runCli.js +45 -0
- package/dist/apps/cli/tests/ids-output.contract.test.js +30 -0
- package/dist/apps/cli/tests/payload-stdin.contract.test.js +15 -0
- package/dist/apps/cli/tests/read-search.contract.test.js +22 -0
- package/dist/apps/cli/tests/ws-health.contract.test.js +36 -0
- package/dist/apps/cli/vitest.config.js +7 -0
- package/dist/main.js +101037 -0
- package/dist/packages/mcp/src/public.js +18 -0
- package/dist/packages/mcp/src/queue/dao.js +165 -0
- package/dist/packages/mcp/src/queue/db.js +26 -0
- package/dist/packages/mcp/src/tools/executeSearchQuery.js +914 -0
- package/dist/packages/mcp/src/tools/findRemsByReference.js +447 -0
- package/dist/packages/mcp/src/tools/getRemConnections.js +566 -0
- package/dist/packages/mcp/src/tools/inspectRemDoc.js +60 -0
- package/dist/packages/mcp/src/tools/listRemBackups.js +35 -0
- package/dist/packages/mcp/src/tools/listRemReferences.js +421 -0
- package/dist/packages/mcp/src/tools/listSupportedOps.js +41 -0
- package/dist/packages/mcp/src/tools/listTodos.js +815 -0
- package/dist/packages/mcp/src/tools/outlineRemSubtree.js +203 -0
- package/dist/packages/mcp/src/tools/readRemTable.js +252 -0
- package/dist/packages/mcp/src/tools/resolveRemReference.js +174 -0
- package/dist/packages/mcp/src/tools/searchQueryTypes.js +127 -0
- package/dist/packages/mcp/src/tools/searchRemOverview.js +422 -0
- package/dist/packages/mcp/src/tools/searchUtils.js +32 -0
- package/dist/packages/mcp/src/tools/shared.js +393 -0
- package/dist/packages/mcp/src/tools/summarizeDailyNotes.js +221 -0
- package/dist/packages/mcp/src/tools/summarizeTopicActivity.js +605 -0
- package/dist/packages/mcp/src/tools/timeFilters.js +130 -0
- package/dist/packages/mcp/src/ws/bridge.js +377 -0
- package/package.json +40 -8
- package/README.md +0 -3
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -5
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import * as Context from 'effect/Context';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Layer from 'effect/Layer';
|
|
4
|
+
import { executeSearchRemOverview } from '../adapters/mcp.js';
|
|
5
|
+
import { AppConfig } from './AppConfig.js';
|
|
6
|
+
import { CliError } from './Errors.js';
|
|
7
|
+
export class RefResolver extends Context.Tag('RefResolver')() {
|
|
8
|
+
}
|
|
9
|
+
function stripQuotes(s) {
|
|
10
|
+
const t = s.trim();
|
|
11
|
+
if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
|
|
12
|
+
return t.slice(1, -1);
|
|
13
|
+
}
|
|
14
|
+
return t;
|
|
15
|
+
}
|
|
16
|
+
function parseRef(input) {
|
|
17
|
+
const raw = input.trim();
|
|
18
|
+
const idx = raw.indexOf(':');
|
|
19
|
+
if (idx <= 0) {
|
|
20
|
+
throw new CliError({
|
|
21
|
+
code: 'INVALID_ARGS',
|
|
22
|
+
message: `ref 不合法:${input}`,
|
|
23
|
+
exitCode: 2,
|
|
24
|
+
hint: ['示例:--ref id:xxx', '示例:--ref title:Demo', '示例:--ref daily:today', '示例:--ref daily:-1'],
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const kind = raw.slice(0, idx).trim();
|
|
28
|
+
const value = stripQuotes(raw.slice(idx + 1));
|
|
29
|
+
if (!value) {
|
|
30
|
+
throw new CliError({
|
|
31
|
+
code: 'INVALID_ARGS',
|
|
32
|
+
message: `ref 不合法(缺少值):${input}`,
|
|
33
|
+
exitCode: 2,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (kind === 'id' || kind === 'title' || kind === 'daily') {
|
|
37
|
+
return { kind, value };
|
|
38
|
+
}
|
|
39
|
+
throw new CliError({
|
|
40
|
+
code: 'INVALID_ARGS',
|
|
41
|
+
message: `ref 不支持:${input}`,
|
|
42
|
+
exitCode: 2,
|
|
43
|
+
hint: ['支持:id:/title:/daily:'],
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function parseDailyOffset(value) {
|
|
47
|
+
const v = value.trim().toLowerCase();
|
|
48
|
+
if (v === 'today' || v === 'now' || v === '0')
|
|
49
|
+
return 0;
|
|
50
|
+
if (v === 'yesterday')
|
|
51
|
+
return -1;
|
|
52
|
+
if (v === 'tomorrow')
|
|
53
|
+
return 1;
|
|
54
|
+
const n = Number.parseInt(v, 10);
|
|
55
|
+
if (!Number.isFinite(n)) {
|
|
56
|
+
throw new CliError({
|
|
57
|
+
code: 'INVALID_ARGS',
|
|
58
|
+
message: `daily ref 不合法:${value}(需要 today/yesterday/tomorrow 或整数偏移)`,
|
|
59
|
+
exitCode: 2,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
return n;
|
|
63
|
+
}
|
|
64
|
+
export const RefResolverLive = Layer.succeed(RefResolver, {
|
|
65
|
+
resolve: (ref) => Effect.gen(function* () {
|
|
66
|
+
const cfg = yield* AppConfig;
|
|
67
|
+
const parsed = yield* Effect.try({
|
|
68
|
+
try: () => parseRef(ref),
|
|
69
|
+
catch: (e) => e && typeof e === 'object' && e._tag === 'CliError'
|
|
70
|
+
? e
|
|
71
|
+
: new CliError({ code: 'INVALID_ARGS', message: `ref 不合法:${ref}`, exitCode: 2 }),
|
|
72
|
+
});
|
|
73
|
+
if (parsed.kind === 'id')
|
|
74
|
+
return parsed.value;
|
|
75
|
+
const dailyOffset = parsed.kind === 'daily'
|
|
76
|
+
? yield* Effect.try({
|
|
77
|
+
try: () => parseDailyOffset(parsed.value),
|
|
78
|
+
catch: (e) => e && typeof e === 'object' && e._tag === 'CliError'
|
|
79
|
+
? e
|
|
80
|
+
: new CliError({ code: 'INVALID_ARGS', message: `daily ref 不合法:${parsed.value}`, exitCode: 2 }),
|
|
81
|
+
})
|
|
82
|
+
: undefined;
|
|
83
|
+
const queryInput = parsed.kind === 'title'
|
|
84
|
+
? { query: parsed.value }
|
|
85
|
+
: { query: 'date', useCurrentDate: true, dateOffsetDays: dailyOffset };
|
|
86
|
+
const result = yield* Effect.tryPromise({
|
|
87
|
+
try: async () => await executeSearchRemOverview({
|
|
88
|
+
...queryInput,
|
|
89
|
+
dbPath: cfg.remnoteDb,
|
|
90
|
+
limit: 1,
|
|
91
|
+
preferExact: true,
|
|
92
|
+
exactFirstSingle: true,
|
|
93
|
+
}),
|
|
94
|
+
catch: (e) => new CliError({
|
|
95
|
+
code: 'DB_UNAVAILABLE',
|
|
96
|
+
message: String(e?.message || e || 'RemNote DB 不可用'),
|
|
97
|
+
exitCode: 1,
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
const first = Array.isArray(result.matches) ? result.matches[0] : undefined;
|
|
101
|
+
const id = first?.id ? String(first.id) : '';
|
|
102
|
+
if (!id) {
|
|
103
|
+
return yield* Effect.fail(new CliError({
|
|
104
|
+
code: 'INVALID_ARGS',
|
|
105
|
+
message: `未找到 ref 对应的 Rem:${ref}`,
|
|
106
|
+
exitCode: 2,
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
return id;
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as Context from 'effect/Context';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Layer from 'effect/Layer';
|
|
4
|
+
import { CliError, isCliError } from './Errors.js';
|
|
5
|
+
import { discoverBackups, withResolvedDatabase } from '../adapters/mcp.js';
|
|
6
|
+
export class RemDb extends Context.Tag('RemDb')() {
|
|
7
|
+
}
|
|
8
|
+
export const RemDbLive = Layer.succeed(RemDb, {
|
|
9
|
+
withDb: (dbPath, fn) => Effect.tryPromise({
|
|
10
|
+
try: async () => await withResolvedDatabase(dbPath, fn),
|
|
11
|
+
catch: (error) => {
|
|
12
|
+
if (isCliError(error))
|
|
13
|
+
return error;
|
|
14
|
+
return new CliError({
|
|
15
|
+
code: 'DB_UNAVAILABLE',
|
|
16
|
+
message: String(error?.message || error || 'RemNote DB 不可用'),
|
|
17
|
+
exitCode: 1,
|
|
18
|
+
details: { db_path: dbPath },
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
}),
|
|
22
|
+
backups: (basePath) => Effect.tryPromise({
|
|
23
|
+
try: async () => await discoverBackups(basePath),
|
|
24
|
+
catch: (error) => {
|
|
25
|
+
if (isCliError(error))
|
|
26
|
+
return error;
|
|
27
|
+
return new CliError({
|
|
28
|
+
code: 'DB_UNAVAILABLE',
|
|
29
|
+
message: String(error?.message || error || '无法读取 RemNote 备份目录'),
|
|
30
|
+
exitCode: 1,
|
|
31
|
+
details: { base_path: basePath },
|
|
32
|
+
});
|
|
33
|
+
},
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import * as Context from 'effect/Context';
|
|
2
|
+
import * as Effect from 'effect/Effect';
|
|
3
|
+
import * as Layer from 'effect/Layer';
|
|
4
|
+
import WebSocket from 'ws';
|
|
5
|
+
import { CliError } from './Errors.js';
|
|
6
|
+
export class WsClient extends Context.Tag('WsClient')() {
|
|
7
|
+
}
|
|
8
|
+
function formatError(e) {
|
|
9
|
+
if (!e)
|
|
10
|
+
return 'unknown error';
|
|
11
|
+
if (typeof e === 'string')
|
|
12
|
+
return e;
|
|
13
|
+
const anyErr = e;
|
|
14
|
+
if (anyErr?.errors && Array.isArray(anyErr.errors)) {
|
|
15
|
+
const parts = anyErr.errors
|
|
16
|
+
.map((inner) => {
|
|
17
|
+
const code = inner?.code ? String(inner.code) : '';
|
|
18
|
+
const msg = inner?.message ? String(inner.message) : String(inner);
|
|
19
|
+
return code ? `${code}: ${msg}` : msg;
|
|
20
|
+
})
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
if (parts.length > 0)
|
|
23
|
+
return `AggregateError(${parts.join('; ')})`;
|
|
24
|
+
}
|
|
25
|
+
if (typeof anyErr?.message === 'string')
|
|
26
|
+
return anyErr.message;
|
|
27
|
+
return String(e);
|
|
28
|
+
}
|
|
29
|
+
function connectOnce(params) {
|
|
30
|
+
return new Promise((resolve, reject) => {
|
|
31
|
+
const ws = new WebSocket(params.url);
|
|
32
|
+
let done = false;
|
|
33
|
+
const finishOk = () => {
|
|
34
|
+
if (done)
|
|
35
|
+
return;
|
|
36
|
+
done = true;
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
try {
|
|
39
|
+
ws.terminate();
|
|
40
|
+
}
|
|
41
|
+
catch (_) { }
|
|
42
|
+
resolve();
|
|
43
|
+
};
|
|
44
|
+
const finishErr = (error) => {
|
|
45
|
+
if (done)
|
|
46
|
+
return;
|
|
47
|
+
done = true;
|
|
48
|
+
clearTimeout(timer);
|
|
49
|
+
try {
|
|
50
|
+
ws.terminate();
|
|
51
|
+
}
|
|
52
|
+
catch (_) { }
|
|
53
|
+
reject(error);
|
|
54
|
+
};
|
|
55
|
+
const timer = setTimeout(() => {
|
|
56
|
+
finishErr(new Error(`timeout after ${params.timeoutMs}ms`));
|
|
57
|
+
}, params.timeoutMs);
|
|
58
|
+
ws.on('open', () => {
|
|
59
|
+
try {
|
|
60
|
+
params.onOpen(ws);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
finishErr(e);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
ws.on('message', (data) => {
|
|
67
|
+
try {
|
|
68
|
+
const txt = String(data);
|
|
69
|
+
const msg = JSON.parse(txt);
|
|
70
|
+
params.onMessage(ws, msg);
|
|
71
|
+
}
|
|
72
|
+
catch (_) {
|
|
73
|
+
// ignore non-json
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
ws.on('error', (e) => {
|
|
77
|
+
finishErr(e);
|
|
78
|
+
});
|
|
79
|
+
ws.on('close', () => {
|
|
80
|
+
finishErr(new Error('connection closed'));
|
|
81
|
+
});
|
|
82
|
+
ws.__finishOk = finishOk;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export const WsClientLive = Layer.succeed(WsClient, {
|
|
86
|
+
health: ({ url, timeoutMs }) => Effect.tryPromise({
|
|
87
|
+
try: async () => {
|
|
88
|
+
const startedAt = Date.now();
|
|
89
|
+
await connectOnce({
|
|
90
|
+
url,
|
|
91
|
+
timeoutMs,
|
|
92
|
+
onOpen: (ws) => {
|
|
93
|
+
ws.send(JSON.stringify({ type: 'Hello' }));
|
|
94
|
+
},
|
|
95
|
+
onMessage: (ws, msg) => {
|
|
96
|
+
if (msg?.type === 'HelloAck' && msg?.ok === true) {
|
|
97
|
+
;
|
|
98
|
+
ws.__finishOk?.();
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
return { url, rtt_ms: Date.now() - startedAt };
|
|
103
|
+
},
|
|
104
|
+
catch: (error) => {
|
|
105
|
+
const message = formatError(error);
|
|
106
|
+
return new CliError({
|
|
107
|
+
code: message.includes('timeout after') ? 'WS_TIMEOUT' : 'WS_UNAVAILABLE',
|
|
108
|
+
message,
|
|
109
|
+
exitCode: 1,
|
|
110
|
+
details: { url, timeout_ms: timeoutMs },
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
triggerStartSync: ({ url, timeoutMs, consumerId }) => Effect.tryPromise({
|
|
115
|
+
try: async () => {
|
|
116
|
+
let result;
|
|
117
|
+
await connectOnce({
|
|
118
|
+
url,
|
|
119
|
+
timeoutMs,
|
|
120
|
+
onOpen: (ws) => {
|
|
121
|
+
ws.send(JSON.stringify({ type: 'TriggerStartSync', consumerId }));
|
|
122
|
+
},
|
|
123
|
+
onMessage: (ws, msg) => {
|
|
124
|
+
if (msg?.type === 'StartSyncTriggered') {
|
|
125
|
+
result = { sent: typeof msg?.sent === 'number' ? msg.sent : 0 };
|
|
126
|
+
ws.__finishOk?.();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return result ?? { sent: 0 };
|
|
131
|
+
},
|
|
132
|
+
catch: (error) => {
|
|
133
|
+
const message = formatError(error);
|
|
134
|
+
return new CliError({
|
|
135
|
+
code: message.includes('timeout after') ? 'WS_TIMEOUT' : 'WS_UNAVAILABLE',
|
|
136
|
+
message,
|
|
137
|
+
exitCode: 1,
|
|
138
|
+
details: { url, timeout_ms: timeoutMs, consumer_id: consumerId },
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
queryClients: ({ url, timeoutMs }) => Effect.tryPromise({
|
|
143
|
+
try: async () => {
|
|
144
|
+
let result;
|
|
145
|
+
await connectOnce({
|
|
146
|
+
url,
|
|
147
|
+
timeoutMs,
|
|
148
|
+
onOpen: (ws) => {
|
|
149
|
+
ws.send(JSON.stringify({ type: 'QueryClients' }));
|
|
150
|
+
},
|
|
151
|
+
onMessage: (ws, msg) => {
|
|
152
|
+
if (msg?.type === 'Clients' && Array.isArray(msg?.clients)) {
|
|
153
|
+
result = { clients: msg.clients };
|
|
154
|
+
ws.__finishOk?.();
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
return result ?? { clients: [] };
|
|
159
|
+
},
|
|
160
|
+
catch: (error) => {
|
|
161
|
+
const message = formatError(error);
|
|
162
|
+
return new CliError({
|
|
163
|
+
code: message.includes('timeout after') ? 'WS_TIMEOUT' : 'WS_UNAVAILABLE',
|
|
164
|
+
message,
|
|
165
|
+
exitCode: 1,
|
|
166
|
+
details: { url, timeout_ms: timeoutMs },
|
|
167
|
+
});
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { runCli } from './helpers/runCli.js';
|
|
3
|
+
describe('cli contract: apply --dry-run --json', () => {
|
|
4
|
+
it('prints ok envelope and does not require queue db', async () => {
|
|
5
|
+
const payload = '[{"type":"create_rem","payload":{"fooBar":1}}]';
|
|
6
|
+
const res = await runCli(['--json', 'apply', '--dry-run', '--payload', payload]);
|
|
7
|
+
expect(res.exitCode).toBe(0);
|
|
8
|
+
expect(res.stderr).toBe('');
|
|
9
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
10
|
+
expect(parsed.ok).toBe(true);
|
|
11
|
+
expect(parsed.data.dry_run).toBe(true);
|
|
12
|
+
expect(Array.isArray(parsed.data.ops)).toBe(true);
|
|
13
|
+
expect(parsed.data.ops[0].type).toBe('create_rem');
|
|
14
|
+
expect(parsed.data.ops[0].payload.foo_bar).toBe(1);
|
|
15
|
+
});
|
|
16
|
+
it('accepts object payload with meta (and normalizes keys)', async () => {
|
|
17
|
+
const payload = JSON.stringify({
|
|
18
|
+
ops: [{ type: 'create_rem', payload: { fooBar: 1 } }],
|
|
19
|
+
meta: { traceId: 't1', fooBar: 2 },
|
|
20
|
+
clientId: 'test-client',
|
|
21
|
+
});
|
|
22
|
+
const res = await runCli(['--json', 'apply', '--dry-run', '--payload', payload]);
|
|
23
|
+
expect(res.exitCode).toBe(0);
|
|
24
|
+
expect(res.stderr).toBe('');
|
|
25
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
26
|
+
expect(parsed.ok).toBe(true);
|
|
27
|
+
expect(parsed.data.dry_run).toBe(true);
|
|
28
|
+
expect(parsed.data.meta.trace_id).toBe('t1');
|
|
29
|
+
expect(parsed.data.meta.foo_bar).toBe(2);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { runCli } from './helpers/runCli.js';
|
|
6
|
+
describe('cli contract: db recent --json', () => {
|
|
7
|
+
it('prints a single json envelope and keeps stderr empty on db error', async () => {
|
|
8
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remnote-cli-test-'));
|
|
9
|
+
const missingDb = path.join(tmpDir, 'missing-remnote.db');
|
|
10
|
+
try {
|
|
11
|
+
const res = await runCli(['--json', '--remnote-db', missingDb, 'db', 'recent']);
|
|
12
|
+
expect(res.exitCode).toBe(1);
|
|
13
|
+
expect(res.stderr).toBe('');
|
|
14
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
15
|
+
expect(parsed.ok).toBe(false);
|
|
16
|
+
expect(parsed.error.code).toBe('DB_UNAVAILABLE');
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { runCli } from './helpers/runCli.js';
|
|
3
|
+
function stripAnsi(input) {
|
|
4
|
+
return input.replace(/\u001b\[[0-9;]*m/g, '');
|
|
5
|
+
}
|
|
6
|
+
describe('cli contract: --help', () => {
|
|
7
|
+
it('prints root help with subcommands', async () => {
|
|
8
|
+
const res = await runCli(['--help']);
|
|
9
|
+
expect(res.exitCode).toBe(0);
|
|
10
|
+
expect(res.stderr).toBe('');
|
|
11
|
+
const out = stripAnsi(res.stdout);
|
|
12
|
+
expect(out).toContain('remnote');
|
|
13
|
+
expect(out).toContain('ws');
|
|
14
|
+
expect(out).toContain('queue');
|
|
15
|
+
expect(out).toContain('apply');
|
|
16
|
+
expect(out).toContain('read');
|
|
17
|
+
expect(out).toContain('write');
|
|
18
|
+
expect(out).toContain('wechat');
|
|
19
|
+
});
|
|
20
|
+
it('prints ws help with subcommands', async () => {
|
|
21
|
+
const res = await runCli(['ws', '--help']);
|
|
22
|
+
expect(res.exitCode).toBe(0);
|
|
23
|
+
expect(res.stderr).toBe('');
|
|
24
|
+
const out = stripAnsi(res.stdout);
|
|
25
|
+
expect(out).toContain('health');
|
|
26
|
+
expect(out).toContain('start');
|
|
27
|
+
expect(out).toContain('stop');
|
|
28
|
+
expect(out).toContain('status');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
export async function runCli(args, options) {
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
+
const __dirname = path.dirname(__filename);
|
|
7
|
+
const repoRoot = path.resolve(__dirname, '../../../../');
|
|
8
|
+
const entry = path.join(repoRoot, 'apps/cli/src/main.ts');
|
|
9
|
+
const timeoutMs = options?.timeoutMs ?? 30_000;
|
|
10
|
+
return await new Promise((resolve) => {
|
|
11
|
+
const child = spawn('bun', ['x', 'tsx', entry, ...args], {
|
|
12
|
+
cwd: repoRoot,
|
|
13
|
+
env: { ...process.env, ...(options?.env ?? {}) },
|
|
14
|
+
stdio: 'pipe',
|
|
15
|
+
});
|
|
16
|
+
let stdout = '';
|
|
17
|
+
let stderr = '';
|
|
18
|
+
child.stdout.setEncoding('utf8');
|
|
19
|
+
child.stderr.setEncoding('utf8');
|
|
20
|
+
child.stdout.on('data', (d) => {
|
|
21
|
+
stdout += d;
|
|
22
|
+
});
|
|
23
|
+
child.stderr.on('data', (d) => {
|
|
24
|
+
stderr += d;
|
|
25
|
+
});
|
|
26
|
+
if (options?.stdin !== undefined) {
|
|
27
|
+
child.stdin.setDefaultEncoding('utf8');
|
|
28
|
+
child.stdin.write(options.stdin);
|
|
29
|
+
child.stdin.end();
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
child.stdin.end();
|
|
33
|
+
}
|
|
34
|
+
const timer = setTimeout(() => {
|
|
35
|
+
try {
|
|
36
|
+
child.kill('SIGKILL');
|
|
37
|
+
}
|
|
38
|
+
catch (_) { }
|
|
39
|
+
}, timeoutMs);
|
|
40
|
+
child.on('close', (code) => {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
resolve({ exitCode: typeof code === 'number' ? code : 1, stdout, stderr });
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { runCli } from './helpers/runCli.js';
|
|
6
|
+
describe('cli contract: --ids output', () => {
|
|
7
|
+
it('prints ids one per line', async () => {
|
|
8
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remnote-cli-test-'));
|
|
9
|
+
const tmpHome = path.join(tmpDir, 'home');
|
|
10
|
+
const queueDb = path.join(tmpDir, 'queue.sqlite');
|
|
11
|
+
try {
|
|
12
|
+
const payload = '[{"type":"create_rem","payload":{"text":"hello"}}]';
|
|
13
|
+
const res = await runCli(['--ids', 'apply', '--payload', payload], {
|
|
14
|
+
env: { HOME: tmpHome, REMNOTE_QUEUE_DB: queueDb },
|
|
15
|
+
});
|
|
16
|
+
expect(res.exitCode).toBe(0);
|
|
17
|
+
expect(res.stderr).toBe('');
|
|
18
|
+
const lines = res.stdout
|
|
19
|
+
.split(/\r?\n/)
|
|
20
|
+
.map((l) => l.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
expect(lines.length).toBe(2);
|
|
23
|
+
expect(lines[0].length).toBeGreaterThan(10);
|
|
24
|
+
expect(lines[1].length).toBeGreaterThan(10);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { runCli } from './helpers/runCli.js';
|
|
3
|
+
describe('cli contract: --payload - stdin', () => {
|
|
4
|
+
it('reads payload from stdin in dry-run mode', async () => {
|
|
5
|
+
const stdin = '[{"type":"create_rem","payload":{"fooBar":1}}]';
|
|
6
|
+
const res = await runCli(['--json', 'apply', '--dry-run', '--payload', '-'], { stdin });
|
|
7
|
+
expect(res.exitCode).toBe(0);
|
|
8
|
+
expect(res.stderr).toBe('');
|
|
9
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
10
|
+
expect(parsed.ok).toBe(true);
|
|
11
|
+
expect(parsed.data.dry_run).toBe(true);
|
|
12
|
+
expect(parsed.data.ops[0].type).toBe('create_rem');
|
|
13
|
+
expect(parsed.data.ops[0].payload.foo_bar).toBe(1);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { runCli } from './helpers/runCli.js';
|
|
6
|
+
describe('cli contract: read search --json', () => {
|
|
7
|
+
it('prints a single json envelope and keeps stderr empty on db error', async () => {
|
|
8
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remnote-cli-test-'));
|
|
9
|
+
const missingDb = path.join(tmpDir, 'missing-remnote.db');
|
|
10
|
+
try {
|
|
11
|
+
const res = await runCli(['--json', '--remnote-db', missingDb, 'read', 'search', '--query', 'hello']);
|
|
12
|
+
expect(res.exitCode).toBe(1);
|
|
13
|
+
expect(res.stderr).toBe('');
|
|
14
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
15
|
+
expect(parsed.ok).toBe(false);
|
|
16
|
+
expect(parsed.error.code).toBe('DB_UNAVAILABLE');
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { promises as fs } from 'node:fs';
|
|
5
|
+
import { startWebSocketBridge } from '../../../packages/mcp/src/ws/bridge.js';
|
|
6
|
+
import { runCli } from './helpers/runCli.js';
|
|
7
|
+
describe('cli contract: ws health --json', () => {
|
|
8
|
+
it('prints ok envelope and health data', async () => {
|
|
9
|
+
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'remnote-cli-test-'));
|
|
10
|
+
const queueDb = path.join(tmpDir, 'queue.sqlite');
|
|
11
|
+
const prevQueueDb = process.env.REMNOTE_QUEUE_DB;
|
|
12
|
+
process.env.REMNOTE_QUEUE_DB = queueDb;
|
|
13
|
+
const started = startWebSocketBridge({ enable: true, port: 0, path: '/ws' });
|
|
14
|
+
expect(started).toBeTruthy();
|
|
15
|
+
try {
|
|
16
|
+
const addr = started.wss.address();
|
|
17
|
+
const port = typeof addr === 'string' ? Number(addr) : addr.port;
|
|
18
|
+
const wsUrl = `ws://localhost:${port}/ws`;
|
|
19
|
+
const res = await runCli(['--json', '--ws-url', wsUrl, 'ws', 'health']);
|
|
20
|
+
expect(res.exitCode).toBe(0);
|
|
21
|
+
expect(res.stderr).toBe('');
|
|
22
|
+
const parsed = JSON.parse(res.stdout.trim());
|
|
23
|
+
expect(parsed.ok).toBe(true);
|
|
24
|
+
expect(parsed.data.url).toBe(wsUrl);
|
|
25
|
+
expect(typeof parsed.data.rtt_ms).toBe('number');
|
|
26
|
+
}
|
|
27
|
+
finally {
|
|
28
|
+
await started.close();
|
|
29
|
+
if (prevQueueDb === undefined)
|
|
30
|
+
delete process.env.REMNOTE_QUEUE_DB;
|
|
31
|
+
else
|
|
32
|
+
process.env.REMNOTE_QUEUE_DB = prevQueueDb;
|
|
33
|
+
await fs.rm(tmpDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
});
|