@melaya/runner 1.1.61 → 1.1.62
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/dist/assistantFiles.d.ts +29 -0
- package/dist/assistantFiles.js +113 -0
- package/dist/assistantHost.py +76 -1
- package/dist/cli.js +7 -0
- package/dist/connection.js +146 -55
- package/dist/folderPicker.d.ts +15 -0
- package/dist/folderPicker.js +69 -0
- package/dist/localRelay.d.ts +8 -1
- package/dist/localRelay.js +21 -1
- package/dist/localVault.py +745 -0
- package/dist/localVaultMirror.py +507 -0
- package/dist/obsidianApp.d.ts +324 -0
- package/dist/obsidianApp.js +1143 -0
- package/dist/obsidianAppCli.d.ts +15 -0
- package/dist/obsidianAppCli.js +123 -0
- package/dist/vaultMirror.d.ts +104 -0
- package/dist/vaultMirror.js +305 -0
- package/dist/vaults.d.ts +350 -0
- package/dist/vaults.js +1130 -0
- package/localVault.py +745 -0
- package/localVaultMirror.py +507 -0
- package/package.json +4 -2
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Obsidian app bridge — the "Runs on: App" tools of
|
|
3
|
+
* docs/plans/obsidian-vault-connector.md §7.1 (§6.3 S1 "App bridge").
|
|
4
|
+
*
|
|
5
|
+
* Some Obsidian features only exist in the running app: file recovery
|
|
6
|
+
* (history, diff), Sync, Publish, the command palette, plugins, themes,
|
|
7
|
+
* snippets, workspaces and tabs, the web viewer, developer tools. For a vault
|
|
8
|
+
* registered on THIS computer the runner reaches the user's own Obsidian
|
|
9
|
+
* through one of two backends:
|
|
10
|
+
*
|
|
11
|
+
* - the official Obsidian CLI (1.12.7+, Settings → General → Command line
|
|
12
|
+
* interface), detected automatically. Always spawned with an ARGUMENT
|
|
13
|
+
* ARRAY (never a shell string), `vault=<the app's id for the registered
|
|
14
|
+
* folder>` first, a deadline and an output cap;
|
|
15
|
+
* - optionally the Local REST API plugin (5.4.0). Its URL and API key are
|
|
16
|
+
* stored on this computer only (~/.melaya-runner/vaults/obsidian-app.json,
|
|
17
|
+
* 0600) and never sent to Melaya, logs or models. The plugin serves a
|
|
18
|
+
* self-signed certificate: its SHA-256 fingerprint is pinned at connect
|
|
19
|
+
* time and every request checks it BEFORE any header (the key) is
|
|
20
|
+
* written. TLS verification is never disabled globally.
|
|
21
|
+
*
|
|
22
|
+
* Endpoints come from the user (the plugin's own settings in the vault, or
|
|
23
|
+
* `melaya-runner obsidian-rest`), never from a model's arguments.
|
|
24
|
+
*
|
|
25
|
+
* Authority (capability, grant, read/write, route, revocation, dev_tools) is
|
|
26
|
+
* checked by vaults.ts before an op reaches this module; the bridge re-checks
|
|
27
|
+
* dev_tools and folder bounds. App ops act on the whole vault, so a grant
|
|
28
|
+
* limited to folders cannot use them (Files+App reads then stay on the files
|
|
29
|
+
* engine, which filters by folder).
|
|
30
|
+
*
|
|
31
|
+
* Never logged: the API key, op arguments, app output.
|
|
32
|
+
*/
|
|
33
|
+
import { type ChildProcess, type SpawnOptions } from "child_process";
|
|
34
|
+
import tls from "tls";
|
|
35
|
+
type ParamKind = "str" | "int" | "flag" | "json" | "code" | "url" | "vpath" | readonly string[];
|
|
36
|
+
/** How an op is served.
|
|
37
|
+
* app only the running app has it (`app_*` ops).
|
|
38
|
+
* prefer Files + App: the app when reachable, else the files engine.
|
|
39
|
+
* untargeted files engine when the call names a note; the app's active note otherwise.
|
|
40
|
+
* fallback files engine first; the app when the engine says unsupported. */
|
|
41
|
+
type Mode = "app" | "prefer" | "untargeted" | "fallback";
|
|
42
|
+
type RestRoute = "version" | "active" | "dataview" | "commands" | "command" | "open";
|
|
43
|
+
export interface AppOpSpec {
|
|
44
|
+
cmd: string | null;
|
|
45
|
+
write: boolean;
|
|
46
|
+
dev?: boolean;
|
|
47
|
+
mode?: Mode;
|
|
48
|
+
params?: Record<string, ParamKind>;
|
|
49
|
+
/** A bare `on|off` positional (`sync on`, `dev:debug off`). */
|
|
50
|
+
state?: "required" | "optional";
|
|
51
|
+
required?: string[];
|
|
52
|
+
/** No `vault=` prefix (app-wide commands). */
|
|
53
|
+
noVault?: boolean;
|
|
54
|
+
/** Append `format=json` unless the caller chose a format. */
|
|
55
|
+
json?: boolean;
|
|
56
|
+
timeoutMs?: number;
|
|
57
|
+
rest?: RestRoute;
|
|
58
|
+
/** Args the files engine accepts for a prefer/fallback op (the rest are stripped on fallback). */
|
|
59
|
+
filesArgs?: readonly string[];
|
|
60
|
+
/** Files+App op without a files implementation. */
|
|
61
|
+
noFiles?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare const APP_OPS: Readonly<Record<string, AppOpSpec>>;
|
|
64
|
+
/** App ops that change something (need "write" in the grant; HITL at the tool). */
|
|
65
|
+
export declare const APP_WRITE_OPS: ReadonlySet<string>;
|
|
66
|
+
/** Developer ops: only when the vault owner turned on "Developer tools". */
|
|
67
|
+
export declare const APP_DEV_OPS: ReadonlySet<string>;
|
|
68
|
+
/** Ops that only the running app serves (app_* plus Files+App ops without a files implementation). */
|
|
69
|
+
export declare const APP_ONLY_OPS: ReadonlySet<string>;
|
|
70
|
+
export declare function isAppOp(op: string): boolean;
|
|
71
|
+
export type AppErrorCode = "app_offline" | "unsupported" | "forbidden" | "forbidden_folder" | "invalid" | "not_found";
|
|
72
|
+
export declare class AppBridgeError extends Error {
|
|
73
|
+
readonly code: AppErrorCode;
|
|
74
|
+
constructor(code: AppErrorCode, message: string);
|
|
75
|
+
}
|
|
76
|
+
export declare function appErr(code: AppErrorCode, message: string): {
|
|
77
|
+
ok: false;
|
|
78
|
+
error: {
|
|
79
|
+
code: AppErrorCode;
|
|
80
|
+
message: string;
|
|
81
|
+
retry_after: null;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
export type AppBackend = "cli" | "rest";
|
|
85
|
+
export declare function appOk(vaultId: string, value: unknown, o: {
|
|
86
|
+
backend: AppBackend;
|
|
87
|
+
truncated?: boolean;
|
|
88
|
+
status?: string;
|
|
89
|
+
extra?: Record<string, unknown>;
|
|
90
|
+
}): {
|
|
91
|
+
ok: true;
|
|
92
|
+
} & Record<string, unknown>;
|
|
93
|
+
/**
|
|
94
|
+
* Build the argv for one CLI call: [`vault=<ref>`], <command>, key=value…,
|
|
95
|
+
* flags, an optional on|off positional. Each value is ONE argv element, so no
|
|
96
|
+
* value can add a parameter, a flag or a shell construct. Unknown arguments
|
|
97
|
+
* are refused (the model sees which).
|
|
98
|
+
*/
|
|
99
|
+
export declare function buildCliArgs(op: string, args: Record<string, unknown>, vaultRef: string | null): {
|
|
100
|
+
ok: true;
|
|
101
|
+
argv: string[];
|
|
102
|
+
} | {
|
|
103
|
+
ok: false;
|
|
104
|
+
message: string;
|
|
105
|
+
};
|
|
106
|
+
export declare const CLI_DEFAULT_TIMEOUT_MS = 30000;
|
|
107
|
+
export declare const CLI_MAX_OUTPUT_BYTES: number;
|
|
108
|
+
export interface CliRunResult {
|
|
109
|
+
code: number | null;
|
|
110
|
+
stdout: string;
|
|
111
|
+
stderr: string;
|
|
112
|
+
timedOut: boolean;
|
|
113
|
+
truncated: boolean;
|
|
114
|
+
}
|
|
115
|
+
export type SpawnFn = (cmd: string, args: string[], opts: SpawnOptions) => ChildProcess;
|
|
116
|
+
/** Run the CLI once: argv array, no shell, deadline, output cap (the process
|
|
117
|
+
* is killed past either). Never rejects. */
|
|
118
|
+
export declare function runCli(bin: string, argv: string[], o?: {
|
|
119
|
+
timeoutMs?: number;
|
|
120
|
+
maxBytes?: number;
|
|
121
|
+
spawnImpl?: SpawnFn;
|
|
122
|
+
cwd?: string;
|
|
123
|
+
}): Promise<CliRunResult>;
|
|
124
|
+
/** Map one CLI result to a value or a typed error. The CLI prints errors on
|
|
125
|
+
* stdout with exit code 0 ("Error: …"), so the text is checked too. */
|
|
126
|
+
export declare function interpretCliOutput(r: CliRunResult, timeoutMs?: number): {
|
|
127
|
+
ok: true;
|
|
128
|
+
value: unknown;
|
|
129
|
+
truncated: boolean;
|
|
130
|
+
} | {
|
|
131
|
+
ok: false;
|
|
132
|
+
code: AppErrorCode;
|
|
133
|
+
message: string;
|
|
134
|
+
};
|
|
135
|
+
export interface HostEnv {
|
|
136
|
+
platform: NodeJS.Platform;
|
|
137
|
+
env: NodeJS.ProcessEnv;
|
|
138
|
+
home: string;
|
|
139
|
+
}
|
|
140
|
+
/** Where the official installer registers the CLI (help: "Command line
|
|
141
|
+
* interface"). MELAYA_OBSIDIAN_CLI overrides it for custom installs; it is
|
|
142
|
+
* local configuration of this computer, never a model argument. */
|
|
143
|
+
export declare function cliCandidates(h: HostEnv): string[];
|
|
144
|
+
/** A launchable CLI binary. Batch files are refused: Windows runs them
|
|
145
|
+
* through cmd.exe, where an argument array is no longer safe. */
|
|
146
|
+
export declare function findCli(h: HostEnv, exists?: (p: string) => boolean): string | null;
|
|
147
|
+
/** obsidian.json: the app's own list of vaults ({vaults: {id: {path, open}}}). */
|
|
148
|
+
export declare function obsidianConfigCandidates(h: HostEnv): string[];
|
|
149
|
+
/** The app's id for a vault folder (what `vault=` takes), or null. */
|
|
150
|
+
export declare function findObsidianVaultId(config: unknown, vaultPath: string, platform: NodeJS.Platform): {
|
|
151
|
+
id: string;
|
|
152
|
+
open: boolean;
|
|
153
|
+
} | null;
|
|
154
|
+
export type RunningProbe = () => Promise<boolean | null>;
|
|
155
|
+
/** Is the Obsidian desktop app running? null = could not tell. Checked first
|
|
156
|
+
* so a read never launches the app as a side effect. */
|
|
157
|
+
export declare function defaultRunningProbe(platform: NodeJS.Platform, spawnImpl?: SpawnFn): RunningProbe;
|
|
158
|
+
export declare const REST_PLUGIN_ID = "obsidian-local-rest-api";
|
|
159
|
+
export interface RestConfig {
|
|
160
|
+
url: string;
|
|
161
|
+
apiKey: string;
|
|
162
|
+
fingerprint256: string;
|
|
163
|
+
headerName?: string;
|
|
164
|
+
connectedAt?: string;
|
|
165
|
+
}
|
|
166
|
+
/** Upper-case hex without separators. */
|
|
167
|
+
export declare function normalizeFingerprint(fp: string): string;
|
|
168
|
+
export declare function fingerprintsEqual(a: string, b: string): boolean;
|
|
169
|
+
/** https on a loopback host only: the bridge never talks to another machine. */
|
|
170
|
+
export declare function parseRestUrl(u: string): {
|
|
171
|
+
host: string;
|
|
172
|
+
port: number;
|
|
173
|
+
} | null;
|
|
174
|
+
export interface PinnedResponse {
|
|
175
|
+
status: number;
|
|
176
|
+
contentType: string;
|
|
177
|
+
body: Buffer;
|
|
178
|
+
}
|
|
179
|
+
export type TlsConnectFn = (opts: tls.ConnectionOptions) => tls.TLSSocket;
|
|
180
|
+
/**
|
|
181
|
+
* One HTTPS request to the plugin with the certificate pinned. The TLS
|
|
182
|
+
* session is opened first; the peer certificate's SHA-256 fingerprint must
|
|
183
|
+
* equal the pinned one before the request (and its Authorization header) is
|
|
184
|
+
* written. A mismatch is a `forbidden` refusal, never a silent retry.
|
|
185
|
+
*/
|
|
186
|
+
export declare function pinnedRequest(cfg: Pick<RestConfig, "url" | "fingerprint256">, req: {
|
|
187
|
+
method: string;
|
|
188
|
+
path: string;
|
|
189
|
+
headers?: Record<string, string>;
|
|
190
|
+
body?: string;
|
|
191
|
+
timeoutMs?: number;
|
|
192
|
+
maxBytes?: number;
|
|
193
|
+
}, connectImpl?: TlsConnectFn): Promise<PinnedResponse>;
|
|
194
|
+
/** SHA-256 of the certificate the endpoint presents (trust-on-first-use for
|
|
195
|
+
* a manual connect; the user confirms it). Sends nothing but a TLS hello. */
|
|
196
|
+
export declare function peerFingerprint(url: string, connectImpl?: TlsConnectFn, timeoutMs?: number): Promise<string>;
|
|
197
|
+
export interface RestPluginSettings {
|
|
198
|
+
port: number;
|
|
199
|
+
apiKey: string;
|
|
200
|
+
certPem: string;
|
|
201
|
+
fingerprint256: string;
|
|
202
|
+
headerName: string;
|
|
203
|
+
}
|
|
204
|
+
/** Read the plugin's own settings from the vault's configuration folder
|
|
205
|
+
* (`<config>/plugins/obsidian-local-rest-api/data.json`): the port, the API
|
|
206
|
+
* key and the certificate it serves, so the fingerprint is pinned from the
|
|
207
|
+
* plugin itself rather than trusted on first use. Local read; nothing leaves
|
|
208
|
+
* this computer. */
|
|
209
|
+
export declare function readRestPluginSettings(vaultRoot: string, configDirName: string): {
|
|
210
|
+
ok: true;
|
|
211
|
+
settings: RestPluginSettings;
|
|
212
|
+
} | {
|
|
213
|
+
ok: false;
|
|
214
|
+
error: string;
|
|
215
|
+
};
|
|
216
|
+
export declare class AppLocalConfig {
|
|
217
|
+
readonly dir: string;
|
|
218
|
+
readonly file: string;
|
|
219
|
+
private cache;
|
|
220
|
+
constructor(dir: string);
|
|
221
|
+
private load;
|
|
222
|
+
getRest(vaultId: string): RestConfig | null;
|
|
223
|
+
setRest(vaultId: string, cfg: RestConfig | null): void;
|
|
224
|
+
}
|
|
225
|
+
/** What the bridge needs of a registered vault. */
|
|
226
|
+
export interface AppVaultRef {
|
|
227
|
+
id: string;
|
|
228
|
+
label: string;
|
|
229
|
+
path: string;
|
|
230
|
+
configDirName: string;
|
|
231
|
+
}
|
|
232
|
+
/** The grant fields the bridge re-checks. */
|
|
233
|
+
export interface AppGrantRef {
|
|
234
|
+
ops: string[];
|
|
235
|
+
folders: string[];
|
|
236
|
+
dev_tools?: boolean;
|
|
237
|
+
}
|
|
238
|
+
export type CliState = "ready" | "not_installed" | "not_running" | "cli_disabled" | "vault_unknown";
|
|
239
|
+
export type RestState = "connected" | "not_configured" | "unreachable" | "cert_changed" | "key_rejected";
|
|
240
|
+
export interface AppStatus {
|
|
241
|
+
cli: CliState;
|
|
242
|
+
rest: RestState;
|
|
243
|
+
version: string | null;
|
|
244
|
+
/** Capability names for the vault report: app_cli, app_rest. */
|
|
245
|
+
features: string[];
|
|
246
|
+
/** Short fingerprint of the pinned certificate (display only). */
|
|
247
|
+
restFingerprint: string | null;
|
|
248
|
+
}
|
|
249
|
+
export type ServeResult = {
|
|
250
|
+
kind: "env";
|
|
251
|
+
env: Record<string, unknown>;
|
|
252
|
+
} | {
|
|
253
|
+
kind: "files";
|
|
254
|
+
args: Record<string, unknown>;
|
|
255
|
+
};
|
|
256
|
+
export interface ObsidianAppBridgeOpts {
|
|
257
|
+
/** Folder for obsidian-app.json (the vault registry's folder). */
|
|
258
|
+
dir: string;
|
|
259
|
+
log?: (m: string) => void;
|
|
260
|
+
host?: HostEnv;
|
|
261
|
+
spawnImpl?: SpawnFn;
|
|
262
|
+
runningProbe?: RunningProbe;
|
|
263
|
+
tlsConnect?: TlsConnectFn;
|
|
264
|
+
fileExists?: (p: string) => boolean;
|
|
265
|
+
/** Override where obsidian.json is read from (tests). */
|
|
266
|
+
readObsidianConfig?: () => unknown;
|
|
267
|
+
}
|
|
268
|
+
export declare class ObsidianAppBridge {
|
|
269
|
+
private readonly o;
|
|
270
|
+
readonly config: AppLocalConfig;
|
|
271
|
+
private readonly h;
|
|
272
|
+
private readonly runningProbe;
|
|
273
|
+
private running;
|
|
274
|
+
private versionProbe;
|
|
275
|
+
private readonly applied;
|
|
276
|
+
private readonly writeChains;
|
|
277
|
+
constructor(o: ObsidianAppBridgeOpts);
|
|
278
|
+
private bin;
|
|
279
|
+
private obsidianConfig;
|
|
280
|
+
private isRunning;
|
|
281
|
+
/** CLI state for one vault + the vault= reference. */
|
|
282
|
+
private cliState;
|
|
283
|
+
private restRequest;
|
|
284
|
+
private restState;
|
|
285
|
+
/** Live App availability for one vault (UI status row, capability report). */
|
|
286
|
+
status(reg: AppVaultRef): Promise<AppStatus>;
|
|
287
|
+
/** Connect the Local REST API of this vault from the plugin's own settings. */
|
|
288
|
+
connectRest(reg: AppVaultRef): Promise<{
|
|
289
|
+
ok: true;
|
|
290
|
+
status: AppStatus;
|
|
291
|
+
} | {
|
|
292
|
+
ok: false;
|
|
293
|
+
error: string;
|
|
294
|
+
}>;
|
|
295
|
+
disconnectRest(vaultId: string): void;
|
|
296
|
+
/** Forget cached probes (after the user changed something). */
|
|
297
|
+
invalidate(): void;
|
|
298
|
+
/**
|
|
299
|
+
* Serve an op through the app, or say it belongs to the files engine.
|
|
300
|
+
* Returns null for ops the bridge does not know. `grant` is the already
|
|
301
|
+
* verified grant (vaults.ts authorizeVaultOp).
|
|
302
|
+
*/
|
|
303
|
+
serve(input: {
|
|
304
|
+
reg: AppVaultRef;
|
|
305
|
+
grant: AppGrantRef;
|
|
306
|
+
op: string;
|
|
307
|
+
args: Record<string, unknown>;
|
|
308
|
+
idempotencyKey?: string | null;
|
|
309
|
+
}): Promise<ServeResult | null>;
|
|
310
|
+
/** base_query: the files engine answered `unsupported`; the live app may serve it. */
|
|
311
|
+
afterFiles(input: {
|
|
312
|
+
reg: AppVaultRef;
|
|
313
|
+
grant: AppGrantRef;
|
|
314
|
+
op: string;
|
|
315
|
+
args: Record<string, unknown>;
|
|
316
|
+
}, filesEnv: unknown): Promise<Record<string, unknown> | null>;
|
|
317
|
+
/** One write at a time per vault; a retried idempotency key returns the stored result. */
|
|
318
|
+
private applyOnce;
|
|
319
|
+
private runViaCli;
|
|
320
|
+
private screenshot;
|
|
321
|
+
private runViaRest;
|
|
322
|
+
}
|
|
323
|
+
export declare function offlineReason(cli: CliState | null, restPossible: boolean): string;
|
|
324
|
+
export {};
|