appduct 0.11.0 → 0.12.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/README.md +4 -4
- package/dist/bin.js +7 -1
- package/dist/cli/result-types.d.ts +5 -4
- package/dist/cli/routes/daemon/run.js +83 -21
- package/dist/cli/routes/daemon/start.js +18 -3
- package/dist/cli/routes/daemon/status.js +18 -3
- package/dist/cli/routes/daemon/stop.js +18 -3
- package/dist/cli/routes/events.js +20 -4
- package/dist/cli/routes/invoke.js +18 -3
- package/dist/cli/routes/keygen.js +1 -0
- package/dist/cli/routes/link.js +18 -3
- package/dist/cli/routes/ls.js +18 -3
- package/dist/cli/routes/mcp.js +33 -19
- package/dist/cli/routes/revoke.js +18 -3
- package/dist/cli/routes/tools.js +18 -3
- package/dist/client/app-client.d.ts +4 -3
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +22 -8
- package/dist/commands/events.d.ts +3 -2
- package/dist/daemon/config.d.ts +4 -1
- package/dist/daemon/daemon.d.ts +3 -0
- package/dist/daemon/event-bus.d.ts +4 -4
- package/dist/daemon/events-log.d.ts +31 -0
- package/dist/daemon/memory-append-only-file.d.ts +17 -0
- package/dist/daemon/node-append-only-file.d.ts +9 -0
- package/dist/daemon/state-dir.d.ts +2 -0
- package/dist/index.js +7 -1
- package/dist/mcp/app-tools.d.ts +7 -7
- package/dist/mcp/events-tool.d.ts +0 -7
- package/dist/rpc/client.d.ts +3 -2
- package/package.json +4 -2
package/dist/cli/routes/tools.js
CHANGED
|
@@ -12,7 +12,7 @@ import { join as join3 } from "node:path";
|
|
|
12
12
|
import { RPC_METHODS } from "@appduct/shared";
|
|
13
13
|
|
|
14
14
|
// src/daemon/log-rotation.ts
|
|
15
|
-
import { chmod, rename, stat } from "node:fs/promises";
|
|
15
|
+
import { chmod as chmod2, rename, stat } from "node:fs/promises";
|
|
16
16
|
|
|
17
17
|
// src/daemon/config.ts
|
|
18
18
|
import { readFile } from "node:fs/promises";
|
|
@@ -25,6 +25,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
25
25
|
"eventBufferSize",
|
|
26
26
|
"auditRetentionDays",
|
|
27
27
|
"daemonLogMaxBytes",
|
|
28
|
+
"eventsLogMaxBytes",
|
|
28
29
|
"policy",
|
|
29
30
|
"advertisedIp",
|
|
30
31
|
"scheme",
|
|
@@ -84,6 +85,7 @@ var defaultConfig = (paths) => {
|
|
|
84
85
|
eventBufferSize: 256,
|
|
85
86
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
86
87
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
88
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
87
89
|
restartDaemonOnVersionMismatch: false,
|
|
88
90
|
policy: {
|
|
89
91
|
default: "allow",
|
|
@@ -146,6 +148,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
146
148
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
147
149
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
148
150
|
}
|
|
151
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
152
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
153
|
+
}
|
|
149
154
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
150
155
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
151
156
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -239,18 +244,27 @@ var isSocketConnectable = (socketPath) => {
|
|
|
239
244
|
|
|
240
245
|
// src/daemon/state-dir.ts
|
|
241
246
|
import { join } from "node:path";
|
|
247
|
+
import { mkdir, chmod } from "node:fs/promises";
|
|
242
248
|
var getStateDirPaths = (stateDir) => {
|
|
243
249
|
return {
|
|
244
250
|
root: stateDir,
|
|
245
251
|
socketPath: join(stateDir, "daemon.sock"),
|
|
246
252
|
pidFilePath: join(stateDir, "daemon.pid"),
|
|
247
253
|
logFilePath: join(stateDir, "daemon.log"),
|
|
254
|
+
eventsLogPath: join(stateDir, "events.log"),
|
|
248
255
|
keyPath: join(stateDir, "key.pem"),
|
|
249
256
|
configPath: join(stateDir, "config.json"),
|
|
250
257
|
auditDir: join(stateDir, "audit"),
|
|
251
258
|
spawnLockPath: join(stateDir, "daemon.spawn.lock")
|
|
252
259
|
};
|
|
253
260
|
};
|
|
261
|
+
var ensureStateDir = async (stateDir) => {
|
|
262
|
+
const paths = getStateDirPaths(stateDir);
|
|
263
|
+
await mkdir(paths.auditDir, { recursive: true });
|
|
264
|
+
await chmod(paths.root, 448);
|
|
265
|
+
await chmod(paths.auditDir, 448);
|
|
266
|
+
return paths;
|
|
267
|
+
};
|
|
254
268
|
|
|
255
269
|
// src/daemon/log-rotation.ts
|
|
256
270
|
var STATE_FILE_MODE = 384;
|
|
@@ -283,7 +297,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
283
297
|
const backupPath = daemonLogBackupPath(options.logFilePath);
|
|
284
298
|
await rename(options.logFilePath, backupPath);
|
|
285
299
|
try {
|
|
286
|
-
await
|
|
300
|
+
await chmod2(backupPath, STATE_FILE_MODE);
|
|
287
301
|
} catch (error) {
|
|
288
302
|
warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
|
|
289
303
|
}
|
|
@@ -299,7 +313,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
299
313
|
var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
|
|
300
314
|
`)) => {
|
|
301
315
|
try {
|
|
302
|
-
await
|
|
316
|
+
await chmod2(logFilePath, STATE_FILE_MODE);
|
|
303
317
|
return true;
|
|
304
318
|
} catch (error) {
|
|
305
319
|
warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
|
|
@@ -640,6 +654,7 @@ var acquireSpawnLock = async (lockPath) => {
|
|
|
640
654
|
return false;
|
|
641
655
|
};
|
|
642
656
|
var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
|
|
657
|
+
await ensureStateDir(paths.root);
|
|
643
658
|
const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
|
|
644
659
|
let lockWaitTimedOut = false;
|
|
645
660
|
if (acquiredLock) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `appduct_wait_for_event` MCP tool (`mcp/events-tool.ts`) — see that module's doc comment for
|
|
8
8
|
* why the notification listener has to be registered before the `events.since` drain call is sent.
|
|
9
9
|
*/
|
|
10
|
-
import { type
|
|
10
|
+
import { type ListedToolDescriptor } from "@appduct/shared";
|
|
11
11
|
import type { DaemonStream } from "../rpc/client.js";
|
|
12
12
|
/** A project's own tool map — declared once, e.g. `type Tools = { sum: { args: { a: number; b:
|
|
13
13
|
* number }; result: { total: number } } }` — to get typed {@link AppClient.call} throughout a test
|
|
@@ -80,8 +80,9 @@ export type AppEvent<TPayload = unknown> = {
|
|
|
80
80
|
};
|
|
81
81
|
export type AppClient<TTools = ToolMap> = {
|
|
82
82
|
readonly sessionId: string;
|
|
83
|
-
/** `tools.list` for this session.
|
|
84
|
-
|
|
83
|
+
/** `tools.list` for this session. An entry spells an ungrouped tool's `group` as `null`,
|
|
84
|
+
* where a registration omits it. */
|
|
85
|
+
tools(): Promise<ListedToolDescriptor[]>;
|
|
85
86
|
/** `tools.call`; rejects with a {@link AppductError} whose `type` preserves the wire error
|
|
86
87
|
* type verbatim (e.g. `"tool_timeout"`, `"policy_denied"`, `"session_suspended"`). */
|
|
87
88
|
call<K extends keyof TTools & string>(name: K, args: ToolArgs<TTools, K>, options?: CallOptions): Promise<ToolResult<TTools, K>>;
|
package/dist/client/index.d.ts
CHANGED
|
@@ -15,4 +15,4 @@ export { connect, type ConnectOptions } from "./connect.js";
|
|
|
15
15
|
export { link, waitForSession, type LinkOptions, type LinkResult, type WaitForSessionOptions } from "./bootstrap.js";
|
|
16
16
|
export { AppductError, type AppductErrorType } from "./errors.js";
|
|
17
17
|
export type { AppClient, AppEvent, CallOptions, EventsOptions, EventsResult, ToolMap, WaitForEventOptions, } from "./app-client.js";
|
|
18
|
-
export type { AgentEndpoint, ErrorType, ToolDescriptor } from "@appduct/shared";
|
|
18
|
+
export type { AgentEndpoint, ErrorType, ListedToolDescriptor, ToolDescriptor } from "@appduct/shared";
|
package/dist/client/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { RPC_METHODS as RPC_METHODS3 } from "@appduct/shared";
|
|
|
4
4
|
// src/daemon/state-dir.ts
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
|
+
import { mkdir, chmod } from "node:fs/promises";
|
|
7
8
|
var resolveStateDir = (override) => {
|
|
8
9
|
return override ?? process.env.APPDUCT_STATE_DIR ?? join(homedir(), ".appduct");
|
|
9
10
|
};
|
|
@@ -13,12 +14,20 @@ var getStateDirPaths = (stateDir) => {
|
|
|
13
14
|
socketPath: join(stateDir, "daemon.sock"),
|
|
14
15
|
pidFilePath: join(stateDir, "daemon.pid"),
|
|
15
16
|
logFilePath: join(stateDir, "daemon.log"),
|
|
17
|
+
eventsLogPath: join(stateDir, "events.log"),
|
|
16
18
|
keyPath: join(stateDir, "key.pem"),
|
|
17
19
|
configPath: join(stateDir, "config.json"),
|
|
18
20
|
auditDir: join(stateDir, "audit"),
|
|
19
21
|
spawnLockPath: join(stateDir, "daemon.spawn.lock")
|
|
20
22
|
};
|
|
21
23
|
};
|
|
24
|
+
var ensureStateDir = async (stateDir) => {
|
|
25
|
+
const paths = getStateDirPaths(stateDir);
|
|
26
|
+
await mkdir(paths.auditDir, { recursive: true });
|
|
27
|
+
await chmod(paths.root, 448);
|
|
28
|
+
await chmod(paths.auditDir, 448);
|
|
29
|
+
return paths;
|
|
30
|
+
};
|
|
22
31
|
|
|
23
32
|
// src/rpc/client.ts
|
|
24
33
|
import { connect as connect2 } from "node:net";
|
|
@@ -28,7 +37,7 @@ import { join as join3 } from "node:path";
|
|
|
28
37
|
import { RPC_METHODS } from "@appduct/shared";
|
|
29
38
|
|
|
30
39
|
// src/daemon/log-rotation.ts
|
|
31
|
-
import { chmod, rename, stat } from "node:fs/promises";
|
|
40
|
+
import { chmod as chmod2, rename, stat } from "node:fs/promises";
|
|
32
41
|
|
|
33
42
|
// src/daemon/config.ts
|
|
34
43
|
import { readFile } from "node:fs/promises";
|
|
@@ -41,6 +50,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
41
50
|
"eventBufferSize",
|
|
42
51
|
"auditRetentionDays",
|
|
43
52
|
"daemonLogMaxBytes",
|
|
53
|
+
"eventsLogMaxBytes",
|
|
44
54
|
"policy",
|
|
45
55
|
"advertisedIp",
|
|
46
56
|
"scheme",
|
|
@@ -100,6 +110,7 @@ var defaultConfig = (paths) => {
|
|
|
100
110
|
eventBufferSize: 256,
|
|
101
111
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
102
112
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
113
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
103
114
|
restartDaemonOnVersionMismatch: false,
|
|
104
115
|
policy: {
|
|
105
116
|
default: "allow",
|
|
@@ -162,6 +173,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
162
173
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
163
174
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
164
175
|
}
|
|
176
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
177
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
178
|
+
}
|
|
165
179
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
166
180
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
167
181
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -284,7 +298,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
284
298
|
const backupPath = daemonLogBackupPath(options.logFilePath);
|
|
285
299
|
await rename(options.logFilePath, backupPath);
|
|
286
300
|
try {
|
|
287
|
-
await
|
|
301
|
+
await chmod2(backupPath, STATE_FILE_MODE);
|
|
288
302
|
} catch (error) {
|
|
289
303
|
warn(`appduct: rotated "${options.logFilePath}" but could not set 0600 on "${backupPath}": ${error.message}`);
|
|
290
304
|
}
|
|
@@ -300,7 +314,7 @@ var rotateDaemonLogIfNeeded = async (options) => {
|
|
|
300
314
|
var ensureDaemonLogMode = async (logFilePath, warn = (message) => process.stderr.write(`${message}
|
|
301
315
|
`)) => {
|
|
302
316
|
try {
|
|
303
|
-
await
|
|
317
|
+
await chmod2(logFilePath, STATE_FILE_MODE);
|
|
304
318
|
return true;
|
|
305
319
|
} catch (error) {
|
|
306
320
|
warn(`appduct: could not set 0600 on "${logFilePath}": ${error.message}`);
|
|
@@ -641,6 +655,7 @@ var acquireSpawnLock = async (lockPath) => {
|
|
|
641
655
|
return false;
|
|
642
656
|
};
|
|
643
657
|
var spawnDaemonAndWait = async (paths, spawn, waitTimeoutMs, pollIntervalMs, options = {}) => {
|
|
658
|
+
await ensureStateDir(paths.root);
|
|
644
659
|
const acquiredLock = await acquireSpawnLock(paths.spawnLockPath);
|
|
645
660
|
let lockWaitTimedOut = false;
|
|
646
661
|
if (acquiredLock) {
|
|
@@ -1096,7 +1111,8 @@ var makeAppClient = (stream, sessionId) => {
|
|
|
1096
1111
|
const result = await stream.call(RPC_METHODS2.toolsList, {
|
|
1097
1112
|
selector: sessionId
|
|
1098
1113
|
});
|
|
1099
|
-
|
|
1114
|
+
const entries = Array.isArray(result) ? result : result.tools;
|
|
1115
|
+
return entries.map((entry) => ({ ...entry, group: entry.group ?? null }));
|
|
1100
1116
|
} catch (error) {
|
|
1101
1117
|
throw toAppductError(error);
|
|
1102
1118
|
}
|
|
@@ -1124,8 +1140,7 @@ var makeAppClient = (stream, sessionId) => {
|
|
|
1124
1140
|
const result = await stream.call(RPC_METHODS2.eventsSince, {
|
|
1125
1141
|
selector: sessionId,
|
|
1126
1142
|
since: options.since,
|
|
1127
|
-
limit: options.limit
|
|
1128
|
-
kinds: ["app_event"]
|
|
1143
|
+
limit: options.limit
|
|
1129
1144
|
});
|
|
1130
1145
|
return {
|
|
1131
1146
|
events: result.events.map((event) => appEventFrom(event, sessionId)).filter((event) => event !== void 0),
|
|
@@ -1184,8 +1199,7 @@ var makeAppClient = (stream, sessionId) => {
|
|
|
1184
1199
|
await stream.call(RPC_METHODS2.eventsSubscribe, { sessionSelector: sessionId, kinds: ["app_event"] });
|
|
1185
1200
|
const sinceResult = await stream.call(RPC_METHODS2.eventsSince, {
|
|
1186
1201
|
selector: sessionId,
|
|
1187
|
-
since: options.since
|
|
1188
|
-
kinds: ["app_event"]
|
|
1202
|
+
since: options.since
|
|
1189
1203
|
});
|
|
1190
1204
|
const seenSeqs = /* @__PURE__ */ new Set();
|
|
1191
1205
|
const backlog = [];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe`
|
|
3
|
-
* connection and streams
|
|
2
|
+
* `appduct events` (ARCHITECTURE.md §10): subscribes to `events.subscribe` for `app_event` only
|
|
3
|
+
* over a persistent connection and streams the app's own events until interrupted (Ctrl-C) or the
|
|
4
|
+
* connection drops. Appduct's lifecycle and tool-call kinds are not printed (issue #98). This is
|
|
4
5
|
* the one command whose output isn't a single rendered `CliResult` — each event is written as it
|
|
5
6
|
* arrives (NDJSON under `--json`, a human line otherwise) — so it plugs into `executeHostedCommand`
|
|
6
7
|
* with a live reporter that suppresses the default one-shot bootstrap render (`cli/runner.ts`).
|
package/dist/daemon/config.d.ts
CHANGED
|
@@ -34,7 +34,7 @@ export type AppductConfig = {
|
|
|
34
34
|
graceSeconds: number;
|
|
35
35
|
linkTtlSeconds: number;
|
|
36
36
|
keepaliveIntervalSeconds: number;
|
|
37
|
-
/** Max retained `app_event
|
|
37
|
+
/** Max retained `app_event`s per session (ARCHITECTURE.md §5's `events.since`
|
|
38
38
|
* retention buffer); default 256. */
|
|
39
39
|
eventBufferSize: number;
|
|
40
40
|
/** Days of `audit/<YYYY-MM-DD>.jsonl` history to keep; files older than this are pruned on
|
|
@@ -43,6 +43,9 @@ export type AppductConfig = {
|
|
|
43
43
|
/** Size at which `daemon.log` is rotated to `daemon.log.1` before a daemon is spawned
|
|
44
44
|
* (ARCHITECTURE.md §3/§4). Default 10 MiB. */
|
|
45
45
|
daemonLogMaxBytes: number;
|
|
46
|
+
/** Size past which the daemon rotates `events.log` to `events.log.1` (ARCHITECTURE.md §3).
|
|
47
|
+
* Default 10 MiB. */
|
|
48
|
+
eventsLogMaxBytes: number;
|
|
46
49
|
policy: AppductPolicyConfig;
|
|
47
50
|
/**
|
|
48
51
|
* When the CLI/MCP client finds the running daemon on a different Appduct version, restart it
|
package/dist/daemon/daemon.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { type DaemonListener } from "./listener.js";
|
|
|
14
14
|
import { type RpcServer } from "./rpc-server.js";
|
|
15
15
|
import { type StateDirPaths } from "./state-dir.js";
|
|
16
16
|
import { type TimerFns } from "./timers.js";
|
|
17
|
+
import { type TlsManager } from "./tls.js";
|
|
17
18
|
/** How often audit retention runs after the startup prune (ARCHITECTURE.md §3: "once a day"). */
|
|
18
19
|
export declare const AUDIT_PRUNE_INTERVAL_MS: number;
|
|
19
20
|
export type DaemonOptions = {
|
|
@@ -33,6 +34,8 @@ export type RunningDaemon = {
|
|
|
33
34
|
startedAt: Date;
|
|
34
35
|
server: RpcServer;
|
|
35
36
|
listener: DaemonListener;
|
|
37
|
+
/** The host certificate the listener serves, so an in-process client can trust it. */
|
|
38
|
+
tls: TlsManager;
|
|
36
39
|
eventBus: EventBus;
|
|
37
40
|
/** Resolves once graceful teardown (sockets closed, RPC/listener closed, pidfile released) completes. */
|
|
38
41
|
exited: Promise<void>;
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* per-session retention ring buffer, and must never let a throwing subscriber take down the daemon
|
|
5
5
|
* or another subscriber.
|
|
6
6
|
*
|
|
7
|
-
* Retention (issue #6): every session-scoped event
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* Retention (issue #6, #98): every session-scoped event gets a monotonically increasing per-session
|
|
8
|
+
* `seq`, but only `app_event` is appended to the per-session ring buffer, capped at `bufferSize`
|
|
9
|
+
* entries (oldest dropped first). Appduct's own kinds (lifecycle, tool calls) are fanned out live
|
|
10
|
+
* and never retained, so no number of tool calls can evict an app event. A
|
|
10
11
|
* session's buffer is discarded the moment it emits a terminal event (`session_expired` /
|
|
11
12
|
* `session_revoked`) — "terminal states free the alias" (sessions.ts) applies to retained events
|
|
12
13
|
* too, matching "no persisted history" (ARCHITECTURE.md §3/§13) — or via an explicit `drop()` for
|
|
@@ -22,7 +23,6 @@ export type EventBusEmitInput = Omit<EventNotification, "ts" | "seq"> & {
|
|
|
22
23
|
export type EventBusListener = (event: EventNotification) => void;
|
|
23
24
|
export type EventsSinceQuery = {
|
|
24
25
|
since?: number;
|
|
25
|
-
kinds?: EventKind[];
|
|
26
26
|
limit?: number;
|
|
27
27
|
};
|
|
28
28
|
export type EventsSinceQueryResult = {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The daemon's own event log, `<stateDir>/events.log` (ARCHITECTURE.md §3): one JSON line per
|
|
3
|
+
* event, for debugging Appduct itself. The daemon decides which events to record; this module only
|
|
4
|
+
* writes them and keeps the file bounded.
|
|
5
|
+
*
|
|
6
|
+
* Writes are serialized on a promise queue, so {@link EventsLog.record} returns at once and never
|
|
7
|
+
* blocks the emitter. After each append the file is rotated to a single backup once it is over
|
|
8
|
+
* `maxBytes`, so the current file is never above the cap once a write has settled. A failed write
|
|
9
|
+
* or rotation is warned, never thrown, and never stops later writes.
|
|
10
|
+
*/
|
|
11
|
+
import type { EventNotification } from "@appduct/shared";
|
|
12
|
+
/** A file that only ever grows by whole lines, with one backup slot. */
|
|
13
|
+
export type AppendOnlyFile = {
|
|
14
|
+
/** Appends `line` plus a newline, creating the file at mode `0600` if it is missing. */
|
|
15
|
+
append(line: string): Promise<void>;
|
|
16
|
+
/** Current size in bytes; `0` when the file does not exist. */
|
|
17
|
+
size(): Promise<number>;
|
|
18
|
+
/** Moves the file to the backup slot, replacing any previous backup. */
|
|
19
|
+
rotate(): Promise<void>;
|
|
20
|
+
};
|
|
21
|
+
export type EventsLog = {
|
|
22
|
+
/** Queues `event` as one JSON line; returns immediately. */
|
|
23
|
+
record(event: EventNotification): void;
|
|
24
|
+
/** Resolves once every write queued so far has settled. */
|
|
25
|
+
flush(): Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
export declare const createEventsLog: (options: {
|
|
28
|
+
file: AppendOnlyFile;
|
|
29
|
+
maxBytes: number;
|
|
30
|
+
warn: (message: string) => void;
|
|
31
|
+
}) => EventsLog;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** In-memory {@link AppendOnlyFile} for tests: inspect the lines written and the backup, or make
|
|
2
|
+
* appends fail. */
|
|
3
|
+
import type { AppendOnlyFile } from "./events-log.js";
|
|
4
|
+
export declare class MemoryAppendOnlyFile implements AppendOnlyFile {
|
|
5
|
+
private current;
|
|
6
|
+
private backup;
|
|
7
|
+
private appendError;
|
|
8
|
+
append(line: string): Promise<void>;
|
|
9
|
+
size(): Promise<number>;
|
|
10
|
+
rotate(): Promise<void>;
|
|
11
|
+
/** Lines in the current file, without their newlines. */
|
|
12
|
+
lines(): string[];
|
|
13
|
+
/** Lines in the backup, without their newlines. */
|
|
14
|
+
backupLines(): string[];
|
|
15
|
+
/** Makes every append throw `error` until called again with `undefined`. */
|
|
16
|
+
failAppendsWith(error: Error | undefined): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** {@link AppendOnlyFile} on the real filesystem; the backup is `<path>.1`. */
|
|
2
|
+
import type { AppendOnlyFile } from "./events-log.js";
|
|
3
|
+
export declare class NodeAppendOnlyFile implements AppendOnlyFile {
|
|
4
|
+
private readonly path;
|
|
5
|
+
constructor(path: string);
|
|
6
|
+
append(line: string): Promise<void>;
|
|
7
|
+
size(): Promise<number>;
|
|
8
|
+
rotate(): Promise<void>;
|
|
9
|
+
}
|
|
@@ -11,6 +11,8 @@ export type StateDirPaths = {
|
|
|
11
11
|
socketPath: string;
|
|
12
12
|
pidFilePath: string;
|
|
13
13
|
logFilePath: string;
|
|
14
|
+
/** The daemon's own event log; rotated to `<eventsLogPath>.1`. */
|
|
15
|
+
eventsLogPath: string;
|
|
14
16
|
keyPath: string;
|
|
15
17
|
configPath: string;
|
|
16
18
|
auditDir: string;
|
package/dist/index.js
CHANGED
|
@@ -140,7 +140,7 @@ var createCli = () => {
|
|
|
140
140
|
"--timeout <ms>",
|
|
141
141
|
"Call timeout in milliseconds. Shortens the deadline; it cannot extend one past the app's own timer, which is the tool's declared timeoutMs (else 10000)."
|
|
142
142
|
);
|
|
143
|
-
cli.command("events [selector]", "Stream
|
|
143
|
+
cli.command("events [selector]", "Stream the events the app posts until interrupted.").option("--follow", "Accepted for script readability; the default behavior already follows.").option("--since <cursor>", "One-shot: print the app events retained since this cursor instead of streaming live.");
|
|
144
144
|
cli.command("revoke [selector]", "Revoke a session.");
|
|
145
145
|
cli.command("mcp", "Start a stdio MCP server that gives MCP clients access to connected apps' tools.").option(
|
|
146
146
|
"--scheme <scheme>",
|
|
@@ -164,6 +164,7 @@ var KNOWN_TOP_LEVEL_KEYS = /* @__PURE__ */ new Set([
|
|
|
164
164
|
"eventBufferSize",
|
|
165
165
|
"auditRetentionDays",
|
|
166
166
|
"daemonLogMaxBytes",
|
|
167
|
+
"eventsLogMaxBytes",
|
|
167
168
|
"policy",
|
|
168
169
|
"advertisedIp",
|
|
169
170
|
"scheme",
|
|
@@ -223,6 +224,7 @@ var defaultConfig = (paths) => {
|
|
|
223
224
|
eventBufferSize: 256,
|
|
224
225
|
auditRetentionDays: DEFAULT_AUDIT_RETENTION_DAYS,
|
|
225
226
|
daemonLogMaxBytes: DEFAULT_DAEMON_LOG_MAX_BYTES,
|
|
227
|
+
eventsLogMaxBytes: 10 * 1024 * 1024,
|
|
226
228
|
restartDaemonOnVersionMismatch: false,
|
|
227
229
|
policy: {
|
|
228
230
|
default: "allow",
|
|
@@ -285,6 +287,9 @@ var loadConfig = async (paths, options = {}) => {
|
|
|
285
287
|
if (parsed.daemonLogMaxBytes !== void 0) {
|
|
286
288
|
config.daemonLogMaxBytes = requirePositiveInteger(parsed.daemonLogMaxBytes, "daemonLogMaxBytes");
|
|
287
289
|
}
|
|
290
|
+
if (parsed.eventsLogMaxBytes !== void 0) {
|
|
291
|
+
config.eventsLogMaxBytes = requirePositiveInteger(parsed.eventsLogMaxBytes, "eventsLogMaxBytes");
|
|
292
|
+
}
|
|
288
293
|
if (parsed.restartDaemonOnVersionMismatch !== void 0) {
|
|
289
294
|
config.restartDaemonOnVersionMismatch = requireBoolean(
|
|
290
295
|
parsed.restartDaemonOnVersionMismatch,
|
|
@@ -333,6 +338,7 @@ var getStateDirPaths = (stateDir) => {
|
|
|
333
338
|
socketPath: join3(stateDir, "daemon.sock"),
|
|
334
339
|
pidFilePath: join3(stateDir, "daemon.pid"),
|
|
335
340
|
logFilePath: join3(stateDir, "daemon.log"),
|
|
341
|
+
eventsLogPath: join3(stateDir, "events.log"),
|
|
336
342
|
keyPath: join3(stateDir, "key.pem"),
|
|
337
343
|
configPath: join3(stateDir, "config.json"),
|
|
338
344
|
auditDir: join3(stateDir, "audit"),
|
package/dist/mcp/app-tools.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Every lookup goes to the daemon live, with no caching: the session and its registry can change
|
|
10
10
|
* between any two calls, and the daemon is the single source of truth for both.
|
|
11
11
|
*/
|
|
12
|
-
import { type EffectivePolicyDecision, type
|
|
12
|
+
import { type EffectivePolicyDecision, type ListedToolDescriptor } from "@appduct/shared";
|
|
13
13
|
import type { DaemonCall } from "./daemon-tools.js";
|
|
14
14
|
export declare const LIST_TOOLS_TOOL_NAME = "appduct_list_tools";
|
|
15
15
|
export declare const DESCRIBE_TOOL_TOOL_NAME = "appduct_describe_tool";
|
|
@@ -103,7 +103,7 @@ export declare const CALL_TOOL_TOOL_DESCRIPTOR: {
|
|
|
103
103
|
export type ResolvedAppTool = {
|
|
104
104
|
sessionId: string;
|
|
105
105
|
alias: string;
|
|
106
|
-
descriptor:
|
|
106
|
+
descriptor: ListedToolDescriptor;
|
|
107
107
|
policy: EffectivePolicyDecision;
|
|
108
108
|
};
|
|
109
109
|
export type CallToolArgs = {
|
|
@@ -132,11 +132,11 @@ export declare const handleListToolsTool: (rawArgs: unknown, call: DaemonCall) =
|
|
|
132
132
|
export declare const handleDescribeToolTool: (rawArgs: unknown, call: DaemonCall) => Promise<{
|
|
133
133
|
name: string;
|
|
134
134
|
description: string;
|
|
135
|
-
input_schema?: import("@appduct/shared").ToolSchemaDescriptor;
|
|
136
|
-
output_schema?: import("@appduct/shared").ToolSchemaDescriptor;
|
|
137
|
-
annotations?: import("@appduct/shared").ToolAnnotations;
|
|
138
|
-
timeout_ms?: number;
|
|
139
|
-
group
|
|
135
|
+
input_schema?: import("@appduct/shared").ToolSchemaDescriptor | undefined;
|
|
136
|
+
output_schema?: import("@appduct/shared").ToolSchemaDescriptor | undefined;
|
|
137
|
+
annotations?: import("@appduct/shared").ToolAnnotations | undefined;
|
|
138
|
+
timeout_ms?: number | undefined;
|
|
139
|
+
group: string | null;
|
|
140
140
|
session: string;
|
|
141
141
|
signature: string;
|
|
142
142
|
policy: EffectivePolicyDecision;
|
|
@@ -38,13 +38,6 @@ export declare const EVENTS_TOOL_DESCRIPTOR: {
|
|
|
38
38
|
readonly type: "integer";
|
|
39
39
|
readonly minimum: 0;
|
|
40
40
|
};
|
|
41
|
-
readonly kinds: {
|
|
42
|
-
readonly type: "array";
|
|
43
|
-
readonly items: {
|
|
44
|
-
readonly type: "string";
|
|
45
|
-
readonly enum: readonly ["daemon_started", "link_created", "link_expired", "session_claimed", "session_suspended", "session_resumed", "session_revoked", "session_expired", "tools_changed", "app_event", "tool_call_started", "tool_call_progress", "tool_call_finished"];
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
41
|
readonly limit: {
|
|
49
42
|
readonly type: "integer";
|
|
50
43
|
readonly exclusiveMinimum: 0;
|
package/dist/rpc/client.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The auto-spawning RPC client library (ARCHITECTURE.md §4, §5) used by the CLI, MCP server, and
|
|
3
|
-
* tests to talk to `daemon.sock`. On `ENOENT`/`ECONNREFUSED` it
|
|
4
|
-
* spawns `daemon run` detached, polls the socket until
|
|
3
|
+
* tests to talk to `daemon.sock`. On `ENOENT`/`ECONNREFUSED` it creates the state dir if it is
|
|
4
|
+
* missing, takes an exclusive spawn-lock, spawns `daemon run` detached, polls the socket until
|
|
5
|
+
* ready, then retries the request once.
|
|
5
6
|
*/
|
|
6
7
|
import { type RpcErrorData } from "@appduct/shared";
|
|
7
8
|
export type SpawnContext = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "appduct",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Driving React Native tools without in-app debug UI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -45,12 +45,13 @@
|
|
|
45
45
|
"reflect-metadata": "^0.2.2",
|
|
46
46
|
"toqr": "^0.1.1",
|
|
47
47
|
"ws": "^8.18.3",
|
|
48
|
-
"@appduct/shared": "0.
|
|
48
|
+
"@appduct/shared": "0.12.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@types/node": "^22.10.0",
|
|
52
52
|
"@types/ws": "^8.18.1",
|
|
53
53
|
"esbuild": "^0.25.12",
|
|
54
|
+
"eslint": "^10",
|
|
54
55
|
"typescript": "~5.9.2"
|
|
55
56
|
},
|
|
56
57
|
"files": [
|
|
@@ -60,6 +61,7 @@
|
|
|
60
61
|
"scripts": {
|
|
61
62
|
"build": "tsc -p tsconfig.build.json && node scripts/bundle.mjs",
|
|
62
63
|
"clean": "rm -rf dist",
|
|
64
|
+
"lint": "eslint src",
|
|
63
65
|
"test": "vitest run --config ../../vitest.config.ts src/__tests__",
|
|
64
66
|
"typecheck": "tsc -p tsconfig.json"
|
|
65
67
|
}
|