create-principles-disciple 1.147.7 → 1.147.9
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/_release/product-identity.json +2 -2
- package/core/dist/host/host-installer.d.ts +15 -0
- package/core/dist/host/host-installer.d.ts.map +1 -1
- package/core/dist/host/host-installer.js.map +1 -1
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.d.ts +37 -0
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.js +126 -0
- package/core/dist/runtime-v2/activation/adaptive-delivery-record.js.map +1 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.d.ts +46 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.d.ts.map +1 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.js +91 -0
- package/core/dist/runtime-v2/activation/adaptive-principle-intervention.js.map +1 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts +2 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.d.ts.map +1 -1
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js +2 -0
- package/core/dist/runtime-v2/activation/prompt-activation-reader-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js +2 -2
- package/core/dist/runtime-v2/feature-flags/feature-flag-lifecycle.js.map +1 -1
- package/core/dist/runtime-v2/index.d.ts +3 -0
- package/core/dist/runtime-v2/index.d.ts.map +1 -1
- package/core/dist/runtime-v2/index.js +2 -0
- package/core/dist/runtime-v2/index.js.map +1 -1
- package/core/dist/runtime-v2/pain-recall-capability.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-recall-capability.js +53 -2
- package/core/dist/runtime-v2/pain-recall-capability.js.map +1 -1
- package/core/dist/runtime-v2/pain-recall-read-model.d.ts.map +1 -1
- package/core/dist/runtime-v2/pain-recall-read-model.js +60 -5
- package/core/dist/runtime-v2/pain-recall-read-model.js.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.d.ts.map +1 -1
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js +35 -0
- package/core/dist/runtime-v2/signal-collector/keyword-stage.js.map +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.js +1 -1
- package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
- package/core/dist/runtime-v2/types/event-types.d.ts +6 -3
- package/core/dist/runtime-v2/types/event-types.d.ts.map +1 -1
- package/core/dist/runtime-v2/types/event-types.js +2 -0
- package/core/dist/runtime-v2/types/event-types.js.map +1 -1
- package/core/package.json +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -29
- package/dist/index.js.map +1 -1
- package/dist/install-summary.d.ts +41 -0
- package/dist/install-summary.d.ts.map +1 -0
- package/dist/install-summary.js +123 -0
- package/dist/install-summary.js.map +1 -0
- package/dist/installer.d.ts +36 -0
- package/dist/installer.d.ts.map +1 -1
- package/dist/installer.js +120 -26
- package/dist/installer.js.map +1 -1
- package/dist/installers/workbuddy-host-installer.d.ts +70 -17
- package/dist/installers/workbuddy-host-installer.d.ts.map +1 -1
- package/dist/installers/workbuddy-host-installer.js +571 -186
- package/dist/installers/workbuddy-host-installer.js.map +1 -1
- package/dist/prompts.js +1 -1
- package/dist/prompts.js.map +1 -1
- package/host-runtime/dist/active-principle-prompt.d.ts +28 -0
- package/host-runtime/dist/active-principle-prompt.js +163 -50
- package/host-runtime/dist/adaptive-principle-revalidation.d.ts +8 -0
- package/host-runtime/dist/adaptive-principle-revalidation.js +52 -0
- package/host-runtime/dist/governance-signal-admission.d.ts +2 -2
- package/host-runtime/dist/governance-signal-admission.js +2 -6
- package/host-runtime/dist/index.d.ts +16 -0
- package/host-runtime/dist/index.js +34 -0
- package/host-runtime/dist/prompt-activation-authority.d.ts +7 -0
- package/host-runtime/dist/prompt-activation-authority.js +16 -0
- package/host-runtime/package.json +1 -1
- package/package.json +3 -3
- package/pd-cli/dist/commands/health-mcp.d.ts +19 -0
- package/pd-cli/dist/commands/health-mcp.d.ts.map +1 -1
- package/pd-cli/dist/commands/health-mcp.js +226 -0
- package/pd-cli/dist/commands/health-mcp.js.map +1 -1
- package/pd-cli/dist/commands/health.d.ts.map +1 -1
- package/pd-cli/dist/commands/health.js +18 -1
- package/pd-cli/dist/commands/health.js.map +1 -1
- package/pd-cli/dist/commands/mcp-logs-export.d.ts +7 -0
- package/pd-cli/dist/commands/mcp-logs-export.d.ts.map +1 -0
- package/pd-cli/dist/commands/mcp-logs-export.js +267 -0
- package/pd-cli/dist/commands/mcp-logs-export.js.map +1 -0
- package/pd-cli/dist/commands/workbuddy-worker.d.ts.map +1 -1
- package/pd-cli/dist/commands/workbuddy-worker.js +2 -1
- package/pd-cli/dist/commands/workbuddy-worker.js.map +1 -1
- package/pd-cli/dist/index.js +35 -0
- package/pd-cli/dist/index.js.map +1 -1
- package/pd-cli/dist/services/zip-writer.d.ts +6 -0
- package/pd-cli/dist/services/zip-writer.d.ts.map +1 -0
- package/pd-cli/dist/services/zip-writer.js +97 -0
- package/pd-cli/dist/services/zip-writer.js.map +1 -0
- package/pd-cli/package.json +1 -1
- package/pd-mcp-server/dist/index.d.ts +1 -0
- package/pd-mcp-server/dist/index.js +167 -10
- package/pd-mcp-server/dist/log-file.d.ts +26 -0
- package/pd-mcp-server/dist/log-file.js +312 -0
- package/pd-mcp-server/dist/logging.d.ts +2 -1
- package/pd-mcp-server/dist/logging.js +25 -14
- package/pd-mcp-server/dist/server.js +78 -24
- package/pd-mcp-server/dist/version.d.ts +1 -1
- package/pd-mcp-server/dist/version.js +1 -1
- package/pd-mcp-server/dist/workspace.d.ts +3 -0
- package/pd-mcp-server/dist/workspace.js +5 -0
- package/pd-mcp-server/package.json +1 -1
- package/plugin/dist/bundle.js +446 -445
- package/plugin/dist/core/event-log.d.ts +3 -0
- package/plugin/dist/governance-audit.js +3 -3
- package/plugin/dist/hooks/adaptive-activation-config.d.ts +8 -0
- package/plugin/dist/hooks/prompt-helpers.d.ts +5 -0
- package/plugin/dist/hooks/prompt-types.d.ts +1 -0
- package/plugin/dist/hooks/prompt.d.ts +8 -1
- package/plugin/dist/host-runtime/openclaw-host-runtime.d.ts +13 -0
- package/plugin/dist/openclaw-sdk.d.ts +4 -0
- package/plugin/dist/openclaw.plugin.json +33 -1
- package/plugin/mcp/launcher.cjs +561 -0
- package/plugin/openclaw.plugin.json +33 -1
- package/plugin/package.json +1 -1
- package/release-manager/dist/installer.js +120 -26
- package/release-manager/dist/installers/workbuddy-host-installer.js +571 -186
- package/release-manager/package.json +1 -1
- package/workbuddy-adapter/dist/pd-hook.js +3 -2
- package/workbuddy-adapter/package.json +1 -1
- package/workbuddy-adapter/plugin/.codebuddy-plugin/plugin.json +1 -1
- package/workbuddy-adapter/plugin/mcp/launcher.cjs +215 -1
- package/workbuddy-adapter/plugin/skills/pd-pain-signal/SKILL.md +4 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal ZIP writer (deflate + UTF-8 names) for the MCP log export command.
|
|
3
|
+
*
|
|
4
|
+
* The repository ships no zip dependency and the export must work offline on
|
|
5
|
+
* an Owner machine; the format needed here is the small, well-specified subset:
|
|
6
|
+
* local headers, one central directory, end-of-central-directory. Archives are
|
|
7
|
+
* bounded by the caller (≤20 MiB) and contain only text files.
|
|
8
|
+
*/
|
|
9
|
+
import { deflateRawSync } from 'node:zlib';
|
|
10
|
+
const LOCAL_FILE_HEADER = 0x04034b50;
|
|
11
|
+
const CENTRAL_DIRECTORY_HEADER = 0x02014b50;
|
|
12
|
+
const END_OF_CENTRAL_DIRECTORY = 0x06054b50;
|
|
13
|
+
const UTF8_FLAG = 0x0800;
|
|
14
|
+
const DEFLATE_METHOD = 8;
|
|
15
|
+
const VERSION_NEEDED = 20;
|
|
16
|
+
const CRC_TABLE = (() => {
|
|
17
|
+
const table = new Uint32Array(256);
|
|
18
|
+
for (let i = 0; i < 256; i += 1) {
|
|
19
|
+
let value = i;
|
|
20
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
21
|
+
value = value & 1 ? 0xedb88320 ^ (value >>> 1) : value >>> 1;
|
|
22
|
+
}
|
|
23
|
+
table[i] = value >>> 0;
|
|
24
|
+
}
|
|
25
|
+
return table;
|
|
26
|
+
})();
|
|
27
|
+
function crc32(buffer) {
|
|
28
|
+
let crc = 0xffffffff;
|
|
29
|
+
for (const byte of buffer) {
|
|
30
|
+
const table = CRC_TABLE[(crc ^ byte) & 0xff] ?? 0;
|
|
31
|
+
crc = table ^ (crc >>> 8);
|
|
32
|
+
}
|
|
33
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
34
|
+
}
|
|
35
|
+
/** DOS date/time (local); fixed to the generation time. */
|
|
36
|
+
function dosDateTime(date) {
|
|
37
|
+
const time = (date.getHours() << 11) | (date.getMinutes() << 5) | Math.floor(date.getSeconds() / 2);
|
|
38
|
+
const dosDate = ((date.getFullYear() - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate();
|
|
39
|
+
return { time, date: dosDate };
|
|
40
|
+
}
|
|
41
|
+
export function buildZip(entries, generatedAt = new Date()) {
|
|
42
|
+
const stamp = dosDateTime(generatedAt);
|
|
43
|
+
const chunks = [];
|
|
44
|
+
const central = [];
|
|
45
|
+
let offset = 0;
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
const name = Buffer.from(entry.name, 'utf8');
|
|
48
|
+
const compressed = deflateRawSync(entry.data);
|
|
49
|
+
const crc = crc32(entry.data);
|
|
50
|
+
const local = Buffer.alloc(30);
|
|
51
|
+
local.writeUInt32LE(LOCAL_FILE_HEADER, 0);
|
|
52
|
+
local.writeUInt16LE(VERSION_NEEDED, 4);
|
|
53
|
+
local.writeUInt16LE(UTF8_FLAG, 6);
|
|
54
|
+
local.writeUInt16LE(DEFLATE_METHOD, 8);
|
|
55
|
+
local.writeUInt16LE(stamp.time, 10);
|
|
56
|
+
local.writeUInt16LE(stamp.date, 12);
|
|
57
|
+
local.writeUInt32LE(crc, 14);
|
|
58
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
59
|
+
local.writeUInt32LE(entry.data.length, 22);
|
|
60
|
+
local.writeUInt16LE(name.length, 26);
|
|
61
|
+
local.writeUInt16LE(0, 28);
|
|
62
|
+
chunks.push(local, name, compressed);
|
|
63
|
+
const directory = Buffer.alloc(46);
|
|
64
|
+
directory.writeUInt32LE(CENTRAL_DIRECTORY_HEADER, 0);
|
|
65
|
+
directory.writeUInt16LE(VERSION_NEEDED, 4);
|
|
66
|
+
directory.writeUInt16LE(VERSION_NEEDED, 6);
|
|
67
|
+
directory.writeUInt16LE(UTF8_FLAG, 8);
|
|
68
|
+
directory.writeUInt16LE(DEFLATE_METHOD, 10);
|
|
69
|
+
directory.writeUInt16LE(stamp.time, 12);
|
|
70
|
+
directory.writeUInt16LE(stamp.date, 14);
|
|
71
|
+
directory.writeUInt32LE(crc, 16);
|
|
72
|
+
directory.writeUInt32LE(compressed.length, 20);
|
|
73
|
+
directory.writeUInt32LE(entry.data.length, 24);
|
|
74
|
+
directory.writeUInt16LE(name.length, 28);
|
|
75
|
+
directory.writeUInt16LE(0, 30);
|
|
76
|
+
directory.writeUInt16LE(0, 32);
|
|
77
|
+
directory.writeUInt16LE(0, 34);
|
|
78
|
+
directory.writeUInt16LE(0, 36);
|
|
79
|
+
directory.writeUInt32LE(0, 38);
|
|
80
|
+
directory.writeUInt32LE(offset, 42);
|
|
81
|
+
central.push(directory, name);
|
|
82
|
+
offset += local.length + name.length + compressed.length;
|
|
83
|
+
}
|
|
84
|
+
const centralOffset = offset;
|
|
85
|
+
const centralSize = central.reduce((size, chunk) => size + chunk.length, 0);
|
|
86
|
+
const end = Buffer.alloc(22);
|
|
87
|
+
end.writeUInt32LE(END_OF_CENTRAL_DIRECTORY, 0);
|
|
88
|
+
end.writeUInt16LE(0, 4);
|
|
89
|
+
end.writeUInt16LE(0, 6);
|
|
90
|
+
end.writeUInt16LE(entries.length, 8);
|
|
91
|
+
end.writeUInt16LE(entries.length, 10);
|
|
92
|
+
end.writeUInt32LE(centralSize, 12);
|
|
93
|
+
end.writeUInt32LE(centralOffset, 16);
|
|
94
|
+
end.writeUInt16LE(0, 20);
|
|
95
|
+
return Buffer.concat([...chunks, ...central, end]);
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=zip-writer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"zip-writer.js","sourceRoot":"","sources":["../../src/services/zip-writer.ts"],"names":[],"mappings":"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"}
|
package/pd-cli/package.json
CHANGED
|
@@ -3,6 +3,7 @@ export { resolveWorkspaceBinding, WORKSPACE_DIR_ENV, HOST_ENV } from './workspac
|
|
|
3
3
|
export { RESPONSE_BUDGET_BYTES, withinBudget, measureTextBytes } from './budget.js';
|
|
4
4
|
export { DEFAULT_DEADLINE_MS, MAX_TRACKED_ORPHANS, runWithDeadline, OrphanLimiter } from './deadline.js';
|
|
5
5
|
export { SERVER_NAME, SERVER_VERSION, MCP_SDK_PIN } from './version.js';
|
|
6
|
+
export { LOG_ENV, LOG_DIR_ENV, LOG_FILE_ENV, LOG_MAX_FILE_BYTES, LOG_MAX_DIR_BYTES, LOG_RETENTION_MS, createMcpFileLog, combineSinks, } from './log-file.js';
|
|
6
7
|
/**
|
|
7
8
|
* Run the stdio server against the real process env. Returns the intended
|
|
8
9
|
* process exit code: 0 after a clean shutdown, 1 for a fail-closed startup.
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
* suppressed), release resources, exit 0.
|
|
12
12
|
*
|
|
13
13
|
* stdout carries MCP protocol messages ONLY (§5.3). This module never
|
|
14
|
-
* console.log's; logs go through the sanitized stderr logger
|
|
14
|
+
* console.log's; logs go through the sanitized stderr logger and, when
|
|
15
|
+
* enabled, the durable JSONL file sink (`~/.pd/logs/mcp`). File logging is
|
|
16
|
+
* strictly observational: every failure is swallowed and can never change a
|
|
17
|
+
* result, the protocol stream, the 32 KiB budget or the 10 s deadline.
|
|
15
18
|
*/
|
|
16
19
|
import { StdioServerTransport } from '@modelcontextprotocol/server/stdio';
|
|
17
20
|
import { createHash } from 'node:crypto';
|
|
@@ -19,27 +22,166 @@ import * as path from 'node:path';
|
|
|
19
22
|
import { pathToFileURL } from 'node:url';
|
|
20
23
|
import { createPdMcpServer } from './server.js';
|
|
21
24
|
import { resolveWorkspaceBinding } from './workspace.js';
|
|
22
|
-
import {
|
|
25
|
+
import { combineSinks, createMcpFileLog } from './log-file.js';
|
|
26
|
+
import { createLogger, stderrSink } from './logging.js';
|
|
23
27
|
export { createPdMcpServer, TOOL_NAME, toolInputSchema } from './server.js';
|
|
24
28
|
export { resolveWorkspaceBinding, WORKSPACE_DIR_ENV, HOST_ENV } from './workspace.js';
|
|
25
29
|
export { RESPONSE_BUDGET_BYTES, withinBudget, measureTextBytes } from './budget.js';
|
|
26
30
|
export { DEFAULT_DEADLINE_MS, MAX_TRACKED_ORPHANS, runWithDeadline, OrphanLimiter } from './deadline.js';
|
|
27
31
|
export { SERVER_NAME, SERVER_VERSION, MCP_SDK_PIN } from './version.js';
|
|
32
|
+
export { LOG_ENV, LOG_DIR_ENV, LOG_FILE_ENV, LOG_MAX_FILE_BYTES, LOG_MAX_DIR_BYTES, LOG_RETENTION_MS, createMcpFileLog, combineSinks, } from './log-file.js';
|
|
33
|
+
function isRecord(value) {
|
|
34
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
/** Bounded, path-free label for client names / versions / protocol ids. */
|
|
37
|
+
function safeLabel(value, max = 48) {
|
|
38
|
+
if (typeof value !== 'string')
|
|
39
|
+
return undefined;
|
|
40
|
+
const cleaned = value.replace(/[^\x20-\x7e]/g, '').replace(/[^\w.#/+:@-]/g, '').slice(0, max);
|
|
41
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
42
|
+
}
|
|
43
|
+
function safeId(value) {
|
|
44
|
+
if (typeof value === 'string')
|
|
45
|
+
return safeLabel(value, 40);
|
|
46
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
47
|
+
return String(value);
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function byteLength(value) {
|
|
51
|
+
try {
|
|
52
|
+
return Buffer.byteLength(JSON.stringify(value) ?? '', 'utf8');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return 0;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const OBSERVED_METHODS = ['initialize', 'tools/list'];
|
|
59
|
+
/**
|
|
60
|
+
* Stdio transport that observes — never alters — the JSON-RPC stream.
|
|
61
|
+
* `initialize` / `tools/list` are otherwise invisible to the server layer:
|
|
62
|
+
* MCP only exposes tool callbacks, so a handshake failure would leave no
|
|
63
|
+
* evidence at all. Observation is wrapped in try/catch and always forwards
|
|
64
|
+
* the untouched message to the SDK.
|
|
65
|
+
*/
|
|
66
|
+
class ObservingStdioTransport extends StdioServerTransport {
|
|
67
|
+
observeLog;
|
|
68
|
+
pending = new Map();
|
|
69
|
+
constructor(observeLog) {
|
|
70
|
+
super();
|
|
71
|
+
this.observeLog = observeLog;
|
|
72
|
+
}
|
|
73
|
+
async start() {
|
|
74
|
+
const inner = this.onmessage;
|
|
75
|
+
this.onmessage = (message) => {
|
|
76
|
+
try {
|
|
77
|
+
this.observeInbound(message);
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Observation must never break the protocol.
|
|
81
|
+
}
|
|
82
|
+
inner?.(message);
|
|
83
|
+
};
|
|
84
|
+
return super.start();
|
|
85
|
+
}
|
|
86
|
+
async send(message) {
|
|
87
|
+
try {
|
|
88
|
+
this.observeOutbound(message);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Observation must never break the protocol.
|
|
92
|
+
}
|
|
93
|
+
return super.send(message);
|
|
94
|
+
}
|
|
95
|
+
observeInbound(message) {
|
|
96
|
+
if (!isRecord(message) || typeof message.method !== 'string')
|
|
97
|
+
return;
|
|
98
|
+
const { method } = message;
|
|
99
|
+
if (!OBSERVED_METHODS.includes(method))
|
|
100
|
+
return;
|
|
101
|
+
const id = safeId(message.id);
|
|
102
|
+
if (id === undefined)
|
|
103
|
+
return;
|
|
104
|
+
const params = isRecord(message.params) ? message.params : {};
|
|
105
|
+
const clientInfo = isRecord(params.clientInfo) ? params.clientInfo : {};
|
|
106
|
+
const capabilities = isRecord(params.capabilities) ? params.capabilities : {};
|
|
107
|
+
this.pending.set(id, {
|
|
108
|
+
method: method === 'initialize' ? 'initialize' : 'tools/list',
|
|
109
|
+
startedAt: Date.now(),
|
|
110
|
+
});
|
|
111
|
+
if (method === 'initialize') {
|
|
112
|
+
this.observeLog('info', 'initialize', {
|
|
113
|
+
phase: 'request',
|
|
114
|
+
requestId: id,
|
|
115
|
+
clientName: safeLabel(clientInfo.name) ?? 'unknown',
|
|
116
|
+
clientVersion: safeLabel(clientInfo.version) ?? 'unknown',
|
|
117
|
+
protocolVersion: safeLabel(params.protocolVersion) ?? 'unknown',
|
|
118
|
+
capabilityKeysCount: Object.keys(capabilities).length,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
this.observeLog('info', 'tools_list', { phase: 'request', requestId: id });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
observeOutbound(message) {
|
|
126
|
+
if (!isRecord(message))
|
|
127
|
+
return;
|
|
128
|
+
const id = safeId(message.id);
|
|
129
|
+
if (id === undefined)
|
|
130
|
+
return;
|
|
131
|
+
const pending = this.pending.get(id);
|
|
132
|
+
if (pending === undefined)
|
|
133
|
+
return;
|
|
134
|
+
this.pending.delete(id);
|
|
135
|
+
const error = isRecord(message.error) ? message.error : undefined;
|
|
136
|
+
const result = isRecord(message.result) ? message.result : undefined;
|
|
137
|
+
const ok = error === undefined;
|
|
138
|
+
const baseFields = {
|
|
139
|
+
phase: 'response',
|
|
140
|
+
requestId: id,
|
|
141
|
+
ok,
|
|
142
|
+
durationMs: Date.now() - pending.startedAt,
|
|
143
|
+
responseBytes: byteLength(message),
|
|
144
|
+
};
|
|
145
|
+
if (pending.method === 'initialize') {
|
|
146
|
+
const fields = { ...baseFields };
|
|
147
|
+
if (!ok)
|
|
148
|
+
fields.errorCode = safeId(error.code) ?? 'unknown_error';
|
|
149
|
+
this.observeLog(ok ? 'info' : 'error', 'initialize', fields);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const tools = Array.isArray(result?.tools) ? result?.tools : undefined;
|
|
153
|
+
const fields = {
|
|
154
|
+
...baseFields,
|
|
155
|
+
toolCount: tools?.length ?? 0,
|
|
156
|
+
};
|
|
157
|
+
if (!ok)
|
|
158
|
+
fields.errorCode = safeId(error.code) ?? 'unknown_error';
|
|
159
|
+
this.observeLog(ok ? 'info' : 'error', 'tools_list', fields);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
28
163
|
/**
|
|
29
164
|
* Run the stdio server against the real process env. Returns the intended
|
|
30
165
|
* process exit code: 0 after a clean shutdown, 1 for a fail-closed startup.
|
|
31
166
|
*/
|
|
32
167
|
export async function main(env = process.env) {
|
|
168
|
+
const fileLog = createMcpFileLog(env);
|
|
169
|
+
const sink = fileLog.enabled ? combineSinks(stderrSink, fileLog.sink) : stderrSink;
|
|
170
|
+
const log = createLogger(sink);
|
|
33
171
|
const binding = resolveWorkspaceBinding(env);
|
|
34
172
|
if (!binding.ok) {
|
|
35
173
|
// Fail-closed BEFORE initialize: one sanitized stderr line with the
|
|
36
|
-
// reason and the next action; nothing about values or paths.
|
|
37
|
-
|
|
38
|
-
|
|
174
|
+
// reason and the next action; nothing about values or paths. The same
|
|
175
|
+
// fact lands on disk as `startup_failure` so a host that cannot even
|
|
176
|
+
// reach `initialize` still leaves PD-side evidence.
|
|
177
|
+
log('error', 'startup_failure', {
|
|
178
|
+
stage: 'server',
|
|
39
179
|
reasonCategory: 'invalid_binding',
|
|
40
|
-
|
|
180
|
+
reasonCode: binding.reasonCode,
|
|
181
|
+
envVar: binding.envVar,
|
|
41
182
|
nextAction: binding.nextAction,
|
|
42
183
|
});
|
|
184
|
+
fileLog.flushSync();
|
|
43
185
|
return 1;
|
|
44
186
|
}
|
|
45
187
|
// Workspace fingerprint (NOT the path): lets operators and acceptance runs
|
|
@@ -50,16 +192,31 @@ export async function main(env = process.env) {
|
|
|
50
192
|
.update(path.resolve(binding.binding.workspaceDir))
|
|
51
193
|
.digest('hex')
|
|
52
194
|
.slice(0, 16);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
195
|
+
log('info', 'startup', {
|
|
196
|
+
host: binding.binding.host ?? 'unspecified',
|
|
197
|
+
workspaceFingerprint,
|
|
198
|
+
nodeVersion: process.version,
|
|
199
|
+
platform: process.platform,
|
|
200
|
+
});
|
|
201
|
+
const handle = createPdMcpServer({
|
|
202
|
+
workspaceDir: binding.binding.workspaceDir,
|
|
203
|
+
host: binding.binding.host,
|
|
204
|
+
logSink: sink,
|
|
205
|
+
});
|
|
206
|
+
const transport = new ObservingStdioTransport(log);
|
|
56
207
|
await handle.mcpServer.connect(transport);
|
|
57
208
|
// Host-driven shutdown: stdin close → SDK closes the transport → we stop
|
|
58
209
|
// accepting new work, let in-flight calls drain (bounded by the deadline)
|
|
59
210
|
// and exit cleanly. The transport close callback is the single trigger.
|
|
60
211
|
transport.onclose = () => {
|
|
61
212
|
handle.beginShutdown();
|
|
62
|
-
|
|
213
|
+
log('info', 'shutdown', {
|
|
214
|
+
reason: 'stdin_closed',
|
|
215
|
+
orphans: handle.orphans.size,
|
|
216
|
+
active: handle.admissions.active,
|
|
217
|
+
uptimeMs: Math.round(process.uptime() * 1000),
|
|
218
|
+
});
|
|
219
|
+
fileLog.flushSync();
|
|
63
220
|
};
|
|
64
221
|
return 0;
|
|
65
222
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export declare const LOG_ENV: 'PD_MCP_LOG';
|
|
2
|
+
export declare const LOG_DIR_ENV: 'PD_MCP_LOG_DIR';
|
|
3
|
+
/** Internal launcher → server contract (basename only, never a path). */
|
|
4
|
+
export declare const LOG_FILE_ENV: 'PD_MCP_LOG_FILE';
|
|
5
|
+
export declare const LOG_RUN_ID_ENV: 'PD_MCP_LOG_RUN_ID';
|
|
6
|
+
export declare const LOG_SEQ_ENV: 'PD_MCP_LOG_SEQ';
|
|
7
|
+
export declare const LOG_MAX_FILE_BYTES: number;
|
|
8
|
+
export declare const LOG_MAX_DIR_BYTES: number;
|
|
9
|
+
export declare const LOG_RETENTION_MS: number;
|
|
10
|
+
export declare const LOG_MAX_SHARDS = 3;
|
|
11
|
+
export interface McpFileLog {
|
|
12
|
+
/** False when `PD_MCP_LOG=0` or the log directory cannot be resolved. */
|
|
13
|
+
readonly enabled: boolean;
|
|
14
|
+
readonly sink: (line: string) => void;
|
|
15
|
+
/** Flush pending lines synchronously; used on startup failure/shutdown. */
|
|
16
|
+
flushSync(): void;
|
|
17
|
+
readonly dir: string | null;
|
|
18
|
+
readonly file: string | null;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create the process log sink. Never throws: an unusable directory degrades to
|
|
22
|
+
* a running-but-silent sink (stderr logging stays untouched).
|
|
23
|
+
*/
|
|
24
|
+
export declare function createMcpFileLog(env?: Record<string, unknown>, clock?: () => Date): McpFileLog;
|
|
25
|
+
/** Compose sinks; a broken sink never prevents the others from running. */
|
|
26
|
+
export declare function combineSinks(...sinks: readonly ((line: string) => void)[]): (line: string) => void;
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Durable JSONL sink for PD MCP observability (default ON).
|
|
3
|
+
*
|
|
4
|
+
* Every entry the process already emits through `createLogger` is also appended
|
|
5
|
+
* to `~/.pd/logs/mcp/mcp-<host>-<pid>-<stamp>.jsonl`. This module is
|
|
6
|
+
* deliberately defensive: no export ever throws, and a failure to write logs
|
|
7
|
+
* can only lose log lines — it can never change a tool result, the stdio
|
|
8
|
+
* protocol stream, the 32 KiB budget or the 10 s deadline.
|
|
9
|
+
*
|
|
10
|
+
* Privacy: the sink only enriches entries the existing scalar-whitelist logger
|
|
11
|
+
* produced. It never accepts bodies, paths or env values; fields are counts,
|
|
12
|
+
* enums, booleans and fingerprints.
|
|
13
|
+
*
|
|
14
|
+
* Limits: one file per process (the launcher opens the file first and passes
|
|
15
|
+
* its basename through the internal env so the server appends instead of
|
|
16
|
+
* opening a second one), 2 MiB per file, at most 3 shards per process, 64 MiB
|
|
17
|
+
* per directory, 14 days retention. Maintenance runs best-effort at open and
|
|
18
|
+
* rotation time.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'node:fs';
|
|
21
|
+
import * as os from 'node:os';
|
|
22
|
+
import * as path from 'node:path';
|
|
23
|
+
import { randomBytes } from 'node:crypto';
|
|
24
|
+
export const LOG_ENV = 'PD_MCP_LOG';
|
|
25
|
+
export const LOG_DIR_ENV = 'PD_MCP_LOG_DIR';
|
|
26
|
+
/** Internal launcher → server contract (basename only, never a path). */
|
|
27
|
+
export const LOG_FILE_ENV = 'PD_MCP_LOG_FILE';
|
|
28
|
+
export const LOG_RUN_ID_ENV = 'PD_MCP_LOG_RUN_ID';
|
|
29
|
+
export const LOG_SEQ_ENV = 'PD_MCP_LOG_SEQ';
|
|
30
|
+
export const LOG_MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
export const LOG_MAX_DIR_BYTES = 64 * 1024 * 1024;
|
|
32
|
+
export const LOG_RETENTION_MS = 14 * 24 * 60 * 60 * 1000;
|
|
33
|
+
export const LOG_MAX_SHARDS = 3;
|
|
34
|
+
const MAX_BUFFER_LINES = 200;
|
|
35
|
+
const MAX_BUFFER_BYTES = 256 * 1024;
|
|
36
|
+
const DISABLED_VALUES = ['0', 'false', 'off'];
|
|
37
|
+
const ADOPTED_FILE = /^mcp-[^\\/:*?"<>|\s]{1,120}\.jsonl$/;
|
|
38
|
+
const RUN_ID = /^[0-9a-f]{8}$/;
|
|
39
|
+
function isRecord(value) {
|
|
40
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
41
|
+
}
|
|
42
|
+
function envString(env, key) {
|
|
43
|
+
const value = env[key];
|
|
44
|
+
return typeof value === 'string' ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
function isEnabled(env) {
|
|
47
|
+
const value = envString(env, LOG_ENV);
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return true;
|
|
50
|
+
return !DISABLED_VALUES.includes(value.trim().toLowerCase());
|
|
51
|
+
}
|
|
52
|
+
function resolveDir(env) {
|
|
53
|
+
const override = envString(env, LOG_DIR_ENV);
|
|
54
|
+
if (override !== undefined && override.trim().length > 0)
|
|
55
|
+
return path.resolve(override);
|
|
56
|
+
try {
|
|
57
|
+
return path.join(os.homedir(), '.pd', 'logs', 'mcp');
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function resolveAdoptedFile(env) {
|
|
64
|
+
const name = envString(env, LOG_FILE_ENV);
|
|
65
|
+
return name !== undefined && ADOPTED_FILE.test(name) ? name : null;
|
|
66
|
+
}
|
|
67
|
+
function resolveRunId(env) {
|
|
68
|
+
const given = envString(env, LOG_RUN_ID_ENV);
|
|
69
|
+
if (given !== undefined && RUN_ID.test(given))
|
|
70
|
+
return given;
|
|
71
|
+
return randomBytes(4).toString('hex');
|
|
72
|
+
}
|
|
73
|
+
function resolveSeq(env) {
|
|
74
|
+
const raw = envString(env, LOG_SEQ_ENV);
|
|
75
|
+
const parsed = raw === undefined ? Number.NaN : Number.parseInt(raw, 10);
|
|
76
|
+
return Number.isInteger(parsed) && parsed >= 0 && parsed < 1_000_000 ? parsed : 0;
|
|
77
|
+
}
|
|
78
|
+
function pad(value, width = 2) {
|
|
79
|
+
return String(value).padStart(width, '0');
|
|
80
|
+
}
|
|
81
|
+
/** RFC3339 with a numeric local offset, plus the offset in minutes. */
|
|
82
|
+
function localTimestamp(date) {
|
|
83
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
84
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
85
|
+
const abs = Math.abs(offsetMinutes);
|
|
86
|
+
const ts = `${pad(date.getFullYear(), 4)}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
87
|
+
`T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}.${pad(date.getMilliseconds(), 3)}` +
|
|
88
|
+
`${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
|
|
89
|
+
return { ts, tzOffsetMinutes: offsetMinutes };
|
|
90
|
+
}
|
|
91
|
+
function utcStamp(date) {
|
|
92
|
+
return (`${pad(date.getUTCFullYear(), 4)}${pad(date.getUTCMonth() + 1)}${pad(date.getUTCDate())}` +
|
|
93
|
+
`T${pad(date.getUTCHours())}${pad(date.getUTCMinutes())}${pad(date.getUTCSeconds())}Z`);
|
|
94
|
+
}
|
|
95
|
+
function listOwnLogFiles(dir) {
|
|
96
|
+
try {
|
|
97
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
98
|
+
const out = [];
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
if (!entry.isFile() || !/^mcp-.*\.jsonl$/.test(entry.name))
|
|
101
|
+
continue;
|
|
102
|
+
const fullPath = path.join(dir, entry.name);
|
|
103
|
+
try {
|
|
104
|
+
const stat = fs.statSync(fullPath);
|
|
105
|
+
out.push({ fullPath, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Concurrent cleanup removed it — ignore.
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return [];
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Best-effort retention + directory cap. Never throws, never deletes non-PD files. */
|
|
118
|
+
function maintainDirectory(dir, nowMs, keepPath) {
|
|
119
|
+
try {
|
|
120
|
+
const files = listOwnLogFiles(dir);
|
|
121
|
+
const survivors = [];
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
if (nowMs - file.mtimeMs > LOG_RETENTION_MS) {
|
|
124
|
+
if (file.fullPath === keepPath) {
|
|
125
|
+
survivors.push(file);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
fs.unlinkSync(file.fullPath);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
survivors.push(file);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
survivors.push(file);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
let total = survivors.reduce((sum, file) => sum + file.size, 0);
|
|
140
|
+
if (total <= LOG_MAX_DIR_BYTES)
|
|
141
|
+
return;
|
|
142
|
+
const removable = survivors
|
|
143
|
+
.filter((file) => file.fullPath !== keepPath)
|
|
144
|
+
.sort((left, right) => left.mtimeMs - right.mtimeMs);
|
|
145
|
+
for (const file of removable) {
|
|
146
|
+
if (total <= LOG_MAX_DIR_BYTES)
|
|
147
|
+
break;
|
|
148
|
+
try {
|
|
149
|
+
fs.unlinkSync(file.fullPath);
|
|
150
|
+
total -= file.size;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
// Another process owns it or it vanished — leave it.
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// Maintenance is advisory only.
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function sanitizeSegment(value) {
|
|
162
|
+
const cleaned = value.replace(/[^A-Za-z0-9_-]/g, '').slice(0, 40);
|
|
163
|
+
return cleaned.length > 0 ? cleaned : 'unknown';
|
|
164
|
+
}
|
|
165
|
+
const NOOP_LOG = {
|
|
166
|
+
enabled: false,
|
|
167
|
+
sink: () => undefined,
|
|
168
|
+
flushSync: () => undefined,
|
|
169
|
+
dir: null,
|
|
170
|
+
file: null,
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Create the process log sink. Never throws: an unusable directory degrades to
|
|
174
|
+
* a running-but-silent sink (stderr logging stays untouched).
|
|
175
|
+
*/
|
|
176
|
+
export function createMcpFileLog(env = process.env, clock = () => new Date()) {
|
|
177
|
+
try {
|
|
178
|
+
if (!isEnabled(env))
|
|
179
|
+
return NOOP_LOG;
|
|
180
|
+
const dir = resolveDir(env);
|
|
181
|
+
if (dir === null)
|
|
182
|
+
return NOOP_LOG;
|
|
183
|
+
const openedAt = clock();
|
|
184
|
+
try {
|
|
185
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
186
|
+
if (!fs.statSync(dir).isDirectory())
|
|
187
|
+
return { ...NOOP_LOG, enabled: true, dir };
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Directory unavailable (e.g. a file occupies the path): stay silent.
|
|
191
|
+
return { ...NOOP_LOG, enabled: true, dir };
|
|
192
|
+
}
|
|
193
|
+
maintainDirectory(dir, openedAt.getTime(), null);
|
|
194
|
+
const host = sanitizeSegment(envString(env, 'PD_MCP_HOST') ?? 'unknown');
|
|
195
|
+
const adopted = resolveAdoptedFile(env);
|
|
196
|
+
const baseName = adopted ?? `mcp-${host}-${process.pid}-${utcStamp(openedAt)}.jsonl`;
|
|
197
|
+
const runId = resolveRunId(env);
|
|
198
|
+
let seq = resolveSeq(env);
|
|
199
|
+
let shard = 1;
|
|
200
|
+
let currentPath = shard === 1 ? path.join(dir, baseName) : path.join(dir, baseName.replace(/\.jsonl$/, `-${shard}.jsonl`));
|
|
201
|
+
let currentBytes = 0;
|
|
202
|
+
try {
|
|
203
|
+
currentBytes = fs.statSync(currentPath).size;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
currentBytes = 0;
|
|
207
|
+
}
|
|
208
|
+
if (currentBytes >= LOG_MAX_FILE_BYTES) {
|
|
209
|
+
shard = 2;
|
|
210
|
+
currentPath = path.join(dir, baseName.replace(/\.jsonl$/, `-${shard}.jsonl`));
|
|
211
|
+
currentBytes = 0;
|
|
212
|
+
}
|
|
213
|
+
let buffer = [];
|
|
214
|
+
let bufferBytes = 0;
|
|
215
|
+
let scheduled = false;
|
|
216
|
+
let exhausted = shard > LOG_MAX_SHARDS;
|
|
217
|
+
function rotate() {
|
|
218
|
+
if (shard >= LOG_MAX_SHARDS) {
|
|
219
|
+
exhausted = true;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
shard += 1;
|
|
223
|
+
currentPath = path.join(dir, baseName.replace(/\.jsonl$/, `-${shard}.jsonl`));
|
|
224
|
+
currentBytes = 0;
|
|
225
|
+
maintainDirectory(dir, clock().getTime(), currentPath);
|
|
226
|
+
}
|
|
227
|
+
function flush() {
|
|
228
|
+
scheduled = false;
|
|
229
|
+
if (buffer.length === 0)
|
|
230
|
+
return;
|
|
231
|
+
const batch = buffer.join('');
|
|
232
|
+
buffer = [];
|
|
233
|
+
bufferBytes = 0;
|
|
234
|
+
try {
|
|
235
|
+
const bytes = Buffer.byteLength(batch, 'utf8');
|
|
236
|
+
if (currentBytes + bytes > LOG_MAX_FILE_BYTES)
|
|
237
|
+
rotate();
|
|
238
|
+
if (exhausted || currentBytes + bytes > LOG_MAX_FILE_BYTES)
|
|
239
|
+
return;
|
|
240
|
+
fs.appendFileSync(currentPath, batch, { mode: 0o600 });
|
|
241
|
+
currentBytes += bytes;
|
|
242
|
+
}
|
|
243
|
+
catch {
|
|
244
|
+
// Drop the batch. Logging must never affect the caller.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function flushSync() {
|
|
248
|
+
try {
|
|
249
|
+
flush();
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
// Swallow even a synchronous flush defect.
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
function sink(line) {
|
|
256
|
+
try {
|
|
257
|
+
const parsed = JSON.parse(line);
|
|
258
|
+
if (!isRecord(parsed))
|
|
259
|
+
return;
|
|
260
|
+
const stamp = localTimestamp(clock());
|
|
261
|
+
parsed.ts = stamp.ts;
|
|
262
|
+
parsed.tzOffsetMinutes = stamp.tzOffsetMinutes;
|
|
263
|
+
parsed.runId = runId;
|
|
264
|
+
parsed.seq = (seq += 1);
|
|
265
|
+
const enriched = JSON.stringify(parsed);
|
|
266
|
+
buffer.push(enriched + '\n');
|
|
267
|
+
bufferBytes += Buffer.byteLength(enriched, 'utf8');
|
|
268
|
+
if (buffer.length >= MAX_BUFFER_LINES || bufferBytes >= MAX_BUFFER_BYTES)
|
|
269
|
+
flushSync();
|
|
270
|
+
else if (!scheduled) {
|
|
271
|
+
scheduled = true;
|
|
272
|
+
setImmediate(() => {
|
|
273
|
+
try {
|
|
274
|
+
flush();
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
// Swallow.
|
|
278
|
+
}
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// Swallow: enrichment/serialization errors must not reach the server.
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
enabled: true,
|
|
288
|
+
sink,
|
|
289
|
+
flushSync,
|
|
290
|
+
dir,
|
|
291
|
+
get file() {
|
|
292
|
+
return currentPath;
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
catch {
|
|
297
|
+
return { ...NOOP_LOG, enabled: true };
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
/** Compose sinks; a broken sink never prevents the others from running. */
|
|
301
|
+
export function combineSinks(...sinks) {
|
|
302
|
+
return (line) => {
|
|
303
|
+
for (const sink of sinks) {
|
|
304
|
+
try {
|
|
305
|
+
sink(line);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// Isolate sinks from each other.
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
}
|