@joe-broadhead/opencode-plaud 0.0.0 → 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +13 -1
- package/dist/index.js +350 -18
- package/package.json +1 -1
- package/skills/opencode-plaud/SKILL.md +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and versions follow [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
4
4
|
|
|
5
|
+
## [0.0.1] - 2026-11-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- The tools RPC, `plaud.recordings.tools`: a script can call Plaud's five read tools with no model, through `opencode api`, as a session and its agent, and with an optional `account` for another account. It gets exactly the access that agent's model has: a tool its rules hide is refused, OpenCode checks each call against the agent's and the session's rules, and an ask is refused at once because no one is there to answer it. Every signed link is withheld, as for the model. `describe` lists the methods with each tool's live input schema. `plaud_export` and any other Plaud tool are never methods. `docs/scripting.md` explains it.
|
|
10
|
+
|
|
5
11
|
## [0.0.0] - 2026-11-01
|
|
6
12
|
|
|
7
13
|
### Added
|
package/README.md
CHANGED
|
@@ -74,6 +74,17 @@ The skills are `opencode-plaud` (the tools and Plaud's data model), `opencode-pl
|
|
|
74
74
|
|
|
75
75
|
See [SECURITY.md](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/SECURITY.md) for saved approvals and other risks.
|
|
76
76
|
|
|
77
|
+
### Scripts
|
|
78
|
+
|
|
79
|
+
A script or a cron job can call Plaud's five read tools with no model, through `opencode api`. It runs as a session and its agent, under that agent's permission rules and the session's own. Add `"account": "<alias>"` to read another account. Exports and every other Plaud tool are never available to scripts, and a script never gets a signed link:
|
|
80
|
+
|
|
81
|
+
```sh
|
|
82
|
+
opencode api post /api/rpc/plaud.recordings.tools/list_files \
|
|
83
|
+
-d '{"input":{"sessionID":"ses_…","input":{"page_size":10}}}'
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
See [Scripting](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/docs/scripting.md).
|
|
87
|
+
|
|
77
88
|
## Permissions
|
|
78
89
|
|
|
79
90
|
The plugin adds default rules to each agent: Plaud's read tools run without a prompt, and `plaud_export` and any other Plaud tool ask first. "Always allow" on an export remembers only that account and kind, such as `default:transcript`. Your own `permissions` rules win over the defaults.
|
|
@@ -134,7 +145,7 @@ Members then only sign in, once per account: run `/connect` and choose **Plaud**
|
|
|
134
145
|
| `/plaud-status` and the palette command are missing | The plugin did not start. Open `/plugins` to see the error. |
|
|
135
146
|
| `Permission plaud_export needs approval` | The run cannot ask. Add an `allow` rule for that export. |
|
|
136
147
|
|
|
137
|
-
Errors from the plugin read `CODE: what happened. Next step: What to do.` Every code is listed in [Errors](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/docs/configuration.md#errors).
|
|
148
|
+
Errors from the plugin read `CODE: what happened. Next step: What to do.` Every code is listed in [Errors](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/docs/configuration.md#errors). A script that uses the tools RPC has its own codes, listed in [Scripting](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/docs/scripting.md#errors).
|
|
138
149
|
|
|
139
150
|
## Uninstall
|
|
140
151
|
|
|
@@ -151,6 +162,7 @@ Run `opencode mcp logout plaud-<alias>` for each extra account too, before you r
|
|
|
151
162
|
|
|
152
163
|
- [Configuration](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/docs/configuration.md)
|
|
153
164
|
- [Architecture](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/docs/architecture.md)
|
|
165
|
+
- [Scripting](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/docs/scripting.md)
|
|
154
166
|
- [Contributing](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/CONTRIBUTING.md)
|
|
155
167
|
- [Security](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/SECURITY.md)
|
|
156
168
|
- [Changelog](https://github.com/joe-broadhead/opencode-plaud/blob/HEAD/CHANGELOG.md)
|
package/dist/index.js
CHANGED
|
@@ -154,9 +154,9 @@ function floatSafeRemainder(val, step) {
|
|
|
154
154
|
return ratio - roundedRatio;
|
|
155
155
|
}
|
|
156
156
|
var EVALUATING = /* @__PURE__ */ Symbol("evaluating");
|
|
157
|
-
function defineLazy(
|
|
157
|
+
function defineLazy(object3, key, getter) {
|
|
158
158
|
let value = void 0;
|
|
159
|
-
Object.defineProperty(
|
|
159
|
+
Object.defineProperty(object3, key, {
|
|
160
160
|
get() {
|
|
161
161
|
if (value === EVALUATING) {
|
|
162
162
|
return void 0;
|
|
@@ -168,7 +168,7 @@ function defineLazy(object2, key, getter) {
|
|
|
168
168
|
return value;
|
|
169
169
|
},
|
|
170
170
|
set(v) {
|
|
171
|
-
Object.defineProperty(
|
|
171
|
+
Object.defineProperty(object3, key, {
|
|
172
172
|
value: v
|
|
173
173
|
// configurable: true,
|
|
174
174
|
});
|
|
@@ -4256,8 +4256,8 @@ function foldObjects(members2) {
|
|
|
4256
4256
|
}
|
|
4257
4257
|
const properties = {};
|
|
4258
4258
|
const required2 = /* @__PURE__ */ new Set();
|
|
4259
|
-
for (const
|
|
4260
|
-
for (const key in
|
|
4259
|
+
for (const object3 of objects) {
|
|
4260
|
+
for (const key in object3.properties) {
|
|
4261
4261
|
if (Object.prototype.hasOwnProperty.call(properties, key))
|
|
4262
4262
|
continue;
|
|
4263
4263
|
const parts = [];
|
|
@@ -4271,18 +4271,18 @@ function foldObjects(members2) {
|
|
|
4271
4271
|
const merged = parts.length === 1 ? parts[0] : foldObjects(parts) ?? { allOf: parts };
|
|
4272
4272
|
assignProp(properties, key, merged);
|
|
4273
4273
|
}
|
|
4274
|
-
for (const key of
|
|
4274
|
+
for (const key of object3.required ?? [])
|
|
4275
4275
|
required2.add(key);
|
|
4276
4276
|
}
|
|
4277
4277
|
const folded = { type: "object", properties };
|
|
4278
4278
|
if (required2.size)
|
|
4279
4279
|
folded.required = [...required2];
|
|
4280
|
-
if (objects.every((
|
|
4280
|
+
if (objects.every((object3) => object3.additionalProperties === false)) {
|
|
4281
4281
|
folded.additionalProperties = false;
|
|
4282
4282
|
} else {
|
|
4283
4283
|
const constraints = [];
|
|
4284
|
-
for (const
|
|
4285
|
-
const constraint = undeclaredConstraint(
|
|
4284
|
+
for (const object3 of objects) {
|
|
4285
|
+
const constraint = undeclaredConstraint(object3);
|
|
4286
4286
|
if (constraint && !constraints.some((seen) => JSON.stringify(seen) === JSON.stringify(constraint)))
|
|
4287
4287
|
constraints.push(constraint);
|
|
4288
4288
|
}
|
|
@@ -6082,6 +6082,9 @@ function withholdSignedLinks(value) {
|
|
|
6082
6082
|
});
|
|
6083
6083
|
return changed ? Object.fromEntries(entries) : value;
|
|
6084
6084
|
}
|
|
6085
|
+
function withholdSignedLinksInOutput(output) {
|
|
6086
|
+
return typeof output === "string" ? withholdSignedLinksInText(output) : withholdSignedLinks(output);
|
|
6087
|
+
}
|
|
6085
6088
|
function withholdSignedLinksInText(text2) {
|
|
6086
6089
|
if (!FIELDS.some((field) => text2.includes(field))) return text2;
|
|
6087
6090
|
const inner = unwrap(text2);
|
|
@@ -6102,6 +6105,9 @@ var KNOWN_READS = ["list_files", "get_file", "get_note", "get_transcript", "get_
|
|
|
6102
6105
|
function serverName(account2) {
|
|
6103
6106
|
return account2 === DEFAULT_ACCOUNT ? SERVER : `${SERVER}-${account2}`;
|
|
6104
6107
|
}
|
|
6108
|
+
function accountServer(aliases) {
|
|
6109
|
+
return (account2) => account2 === void 0 || account2 === DEFAULT_ACCOUNT || aliases.includes(account2) ? serverName(account2 ?? DEFAULT_ACCOUNT) : void 0;
|
|
6110
|
+
}
|
|
6105
6111
|
function mcpServers(accounts) {
|
|
6106
6112
|
return [DEFAULT_ACCOUNT, ...accounts].map((account2) => ({ account: account2, name: serverName(account2) }));
|
|
6107
6113
|
}
|
|
@@ -6218,11 +6224,11 @@ function overallState(status) {
|
|
|
6218
6224
|
var READY_STEP = "Nothing to do.";
|
|
6219
6225
|
function statusLines(status) {
|
|
6220
6226
|
const several = status.accounts.length > 1;
|
|
6221
|
-
const
|
|
6227
|
+
const connection2 = status.accounts.map(
|
|
6222
6228
|
(account2) => [`${several ? `${account2.account}: ` : ""}${STATE_TEXT[account2.state]}`, ...account2.reason === void 0 ? [] : [` (${account2.reason})`]].join("")
|
|
6223
6229
|
);
|
|
6224
6230
|
const lines = [
|
|
6225
|
-
{ label: "Connection", value:
|
|
6231
|
+
{ label: "Connection", value: connection2.join("; "), tone: STATE_TONE[overallState(status)] },
|
|
6226
6232
|
{
|
|
6227
6233
|
label: "Account",
|
|
6228
6234
|
value: status.accounts.map((account2) => `${account2.account} (server ${account2.server}${account2.userServer ? ", from your OpenCode config" : ""})`).join(", "),
|
|
@@ -6430,13 +6436,13 @@ function nameIntegrations(ctx, servers) {
|
|
|
6430
6436
|
}
|
|
6431
6437
|
for (const [id, state] of states) {
|
|
6432
6438
|
if (stopped.signal.aborted) return;
|
|
6433
|
-
const
|
|
6434
|
-
if (
|
|
6435
|
-
const reported =
|
|
6436
|
-
if (state === "needs_auth" && !
|
|
6437
|
-
await ctx.integration.connection.status({ integrationID: id, connection, status: { status: "needs_auth", message: SIGN_IN_AGAIN } });
|
|
6439
|
+
const connection2 = await ctx.integration.connection.active(id);
|
|
6440
|
+
if (connection2?.type !== "credential") continue;
|
|
6441
|
+
const reported = connection2.status?.message === SIGN_IN_AGAIN;
|
|
6442
|
+
if (state === "needs_auth" && !connection2.status) {
|
|
6443
|
+
await ctx.integration.connection.status({ integrationID: id, connection: connection2, status: { status: "needs_auth", message: SIGN_IN_AGAIN } });
|
|
6438
6444
|
} else if (state === "connected" && reported) {
|
|
6439
|
-
await ctx.integration.connection.status({ integrationID: id, connection, status: void 0 });
|
|
6445
|
+
await ctx.integration.connection.status({ integrationID: id, connection: connection2, status: void 0 });
|
|
6440
6446
|
}
|
|
6441
6447
|
}
|
|
6442
6448
|
};
|
|
@@ -6949,7 +6955,7 @@ async function registerTools(ctx, options) {
|
|
|
6949
6955
|
const { output, content } = event.result;
|
|
6950
6956
|
event.result = {
|
|
6951
6957
|
...event.result,
|
|
6952
|
-
...output === void 0 ? {} : { output:
|
|
6958
|
+
...output === void 0 ? {} : { output: withholdSignedLinksInOutput(output) },
|
|
6953
6959
|
...content === void 0 ? {} : {
|
|
6954
6960
|
content: typeof content === "string" ? withholdSignedLinksInText(content) : content.map((part) => part.type === "text" ? { ...part, text: withholdSignedLinksInText(part.text) } : part)
|
|
6955
6961
|
}
|
|
@@ -6963,6 +6969,10 @@ async function registerTools(ctx, options) {
|
|
|
6963
6969
|
};
|
|
6964
6970
|
}
|
|
6965
6971
|
|
|
6972
|
+
// src/host/tools-rpc.ts
|
|
6973
|
+
import { randomUUID } from "node:crypto";
|
|
6974
|
+
import { Rpc as Rpc2 } from "@opencode/plugin/rpc";
|
|
6975
|
+
|
|
6966
6976
|
// src/version.ts
|
|
6967
6977
|
var PLUGIN_ID = "plaud.recordings";
|
|
6968
6978
|
var TESTED_HOST = ">=2.2.3 <2.3.0";
|
|
@@ -6992,6 +7002,327 @@ function checkHost(version2, allowUntestedHost) {
|
|
|
6992
7002
|
return { version: version2, tested: false, warning: `${untested} It was loaded because "allowUntestedHost" is set.` };
|
|
6993
7003
|
}
|
|
6994
7004
|
|
|
7005
|
+
// src/host/tools-rpc.ts
|
|
7006
|
+
var PLUGIN_ID2 = PLUGIN_ID;
|
|
7007
|
+
var KNOWN_READS2 = KNOWN_READS;
|
|
7008
|
+
function shapeResult(_method, result) {
|
|
7009
|
+
const text2 = withholdSignedLinksInText(result.text);
|
|
7010
|
+
return result.output === void 0 ? { text: text2 } : { output: withholdSignedLinksInOutput(result.output), text: text2 };
|
|
7011
|
+
}
|
|
7012
|
+
var NEXT_STEPS = {
|
|
7013
|
+
SESSION_UNKNOWN: `Create a session that names an agent, such as POST /api/session with {"agent":"build"}, and pass its id.`,
|
|
7014
|
+
SESSION_AGENT_REQUIRED: `Create a session that names an agent, such as POST /api/session with {"agent":"build"}, and pass its id.`,
|
|
7015
|
+
PERMISSION_DENIED: "Add an allow rule for the tool to the session's agent or to the session, or use a session whose agent allows it.",
|
|
7016
|
+
SERVER_CONFLICT: "Remove your own server of that name from mcp.servers in opencode.json so that the plugin manages it, then restart OpenCode.",
|
|
7017
|
+
AUTH_REQUIRED: "Run /connect and choose Plaud, or Plaud (<alias>) for another account, then press o or Enter to open the sign-in page.",
|
|
7018
|
+
CONNECTING: "Wait a few seconds while OpenCode connects to the account's server, then call the method again.",
|
|
7019
|
+
CONNECTION_FAILED: "Run /plaud-status and follow its next step.",
|
|
7020
|
+
DISABLED: "Enable the account's server, plaud or plaud-<alias>, in /mcps.",
|
|
7021
|
+
NOT_REGISTERED: "Run /plaud-status and follow its next step.",
|
|
7022
|
+
TOOL_UNAVAILABLE: "Call describe to see which methods OpenCode lists now, and run /plaud-status.",
|
|
7023
|
+
UPSTREAM_ERROR: "Check the input against the method's schema from describe, and run /plaud-status if it keeps failing.",
|
|
7024
|
+
CANCELLED: "Call the method again if you still need the result.",
|
|
7025
|
+
ACCOUNT_UNKNOWN: `Leave "account" out for the default account, or pass "default" or an alias from the plugin's accounts option, as /plaud-status lists them.`
|
|
7026
|
+
};
|
|
7027
|
+
function pluginError(_error) {
|
|
7028
|
+
return void 0;
|
|
7029
|
+
}
|
|
7030
|
+
var TOOLS_RPC_ID = `${PLUGIN_ID2}.tools`;
|
|
7031
|
+
var UNATTENDED = "A script cannot answer an approval prompt; this needs an allow rule for the session's agent.";
|
|
7032
|
+
var MESSAGE_ID = "msg_script";
|
|
7033
|
+
var UPSTREAM_LIMIT = 300;
|
|
7034
|
+
var object2 = (properties, required2) => ({
|
|
7035
|
+
type: "object",
|
|
7036
|
+
properties,
|
|
7037
|
+
required: required2,
|
|
7038
|
+
additionalProperties: false
|
|
7039
|
+
});
|
|
7040
|
+
var STRING = { type: "string" };
|
|
7041
|
+
var ANY_OBJECT = { type: "object" };
|
|
7042
|
+
var NO_INPUT = object2({}, []);
|
|
7043
|
+
var FAILED = object2({ code: STRING, next: STRING }, ["code", "next"]);
|
|
7044
|
+
var mcpOutput = (output = {}) => object2({ output, text: STRING }, ["text"]);
|
|
7045
|
+
var DESCRIBED = object2(
|
|
7046
|
+
{
|
|
7047
|
+
name: STRING,
|
|
7048
|
+
tool: STRING,
|
|
7049
|
+
kind: { type: "string", enum: ["mcp", "plugin"] },
|
|
7050
|
+
action: STRING,
|
|
7051
|
+
description: STRING,
|
|
7052
|
+
available: { type: "boolean" },
|
|
7053
|
+
reason: STRING,
|
|
7054
|
+
input: ANY_OBJECT,
|
|
7055
|
+
output: ANY_OBJECT
|
|
7056
|
+
},
|
|
7057
|
+
["name", "tool", "kind", "action", "description", "available", "input", "output"]
|
|
7058
|
+
);
|
|
7059
|
+
var DESCRIPTION = object2({ id: STRING, call: STRING, location: STRING, methods: { type: "array", items: DESCRIBED } }, [
|
|
7060
|
+
"id",
|
|
7061
|
+
"call",
|
|
7062
|
+
"location",
|
|
7063
|
+
"methods"
|
|
7064
|
+
]);
|
|
7065
|
+
var plainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7066
|
+
function strict(schema, issues) {
|
|
7067
|
+
const keys = Object.keys(schema["properties"]);
|
|
7068
|
+
const validate2 = (value) => {
|
|
7069
|
+
if (!plainObject(value)) return { issues: [{ message: "Expected an object" }] };
|
|
7070
|
+
const found = [...Object.keys(value).filter((key) => !keys.includes(key)).map((key) => `"${key}" is not allowed`), ...issues(value)];
|
|
7071
|
+
return found.length === 0 ? { value } : { issues: found.map((message) => ({ message })) };
|
|
7072
|
+
};
|
|
7073
|
+
return Object.defineProperty({ ...schema }, "~standard", { value: { version: 1, vendor: "tools-rpc", validate: validate2 }, enumerable: false });
|
|
7074
|
+
}
|
|
7075
|
+
var ScriptError = class extends Error {
|
|
7076
|
+
failure;
|
|
7077
|
+
constructor(failure2) {
|
|
7078
|
+
super(`${failure2.code}: ${failure2.what.replace(/[.\s]+$/, "")}. Next step: ${failure2.next}`);
|
|
7079
|
+
this.failure = failure2;
|
|
7080
|
+
}
|
|
7081
|
+
};
|
|
7082
|
+
var fail2 = (code, what) => new ScriptError({ code, what, next: NEXT_STEPS[code] });
|
|
7083
|
+
var record = (value) => typeof value === "object" && value !== null;
|
|
7084
|
+
function notFound(error2, tags) {
|
|
7085
|
+
if (!record(error2)) return false;
|
|
7086
|
+
const tag = error2["_tag"] ?? error2["name"];
|
|
7087
|
+
return typeof tag === "string" && tags.includes(tag) || error2["status"] === 404 || error2["statusCode"] === 404;
|
|
7088
|
+
}
|
|
7089
|
+
var toolError = (error2) => record(error2) && error2["_tag"] === "Tool.Error";
|
|
7090
|
+
var wildcard = (pattern, value) => new RegExp(`^${pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".")}$`).test(value);
|
|
7091
|
+
var hidden = (rules, action) => rules.findLast((rule2) => wildcard(rule2.action, action) && wildcard(rule2.resource, "*"))?.effect === "deny";
|
|
7092
|
+
var SECRETS = [
|
|
7093
|
+
[/\b(bearer|basic|token)\s+[\w.~+/=-]{8,}/gi, "$1 [redacted]"],
|
|
7094
|
+
[/\b([\w-]*(?:api[_-]?key|access[_-]?key|private[_-]?key|token|secret|password|signature|credential)["']?\s*[:=]\s*)["']?[^\s"'&,;]+["']?/gi, "$1[redacted]"],
|
|
7095
|
+
[/\beyJ[\w-]{4,}\.[\w-]{4,}\.[\w-]{4,}/g, "[redacted]"],
|
|
7096
|
+
[/\b(?:tvly|sk|pk|rk|ghp|gho|ghu|ghs|ghr|github_pat|glpat|xox[abposr]|lin_api|lin_oauth)[-_][\w-]{8,}/gi, "[redacted]"],
|
|
7097
|
+
[/[\w+=-]{40,}/g, "[redacted]"]
|
|
7098
|
+
];
|
|
7099
|
+
var withoutQuery = (text2) => text2.replace(/\bhttps?:\/\/[^\s"'<>()]+/gi, (url2) => {
|
|
7100
|
+
const cut = url2.search(/[?#]/);
|
|
7101
|
+
return cut === -1 ? url2 : `${url2.slice(0, cut)}?[redacted]`;
|
|
7102
|
+
});
|
|
7103
|
+
function upstreamText(text2) {
|
|
7104
|
+
const line = text2.split(/\r?\n/).find((part) => part.trim() !== "")?.trim() ?? "no message";
|
|
7105
|
+
const redacted = SECRETS.reduce((value, [pattern, replacement]) => value.replace(pattern, replacement), withoutQuery(line));
|
|
7106
|
+
return redacted.length <= UPSTREAM_LIMIT ? redacted : `${redacted.slice(0, UPSTREAM_LIMIT - 1)}\u2026`;
|
|
7107
|
+
}
|
|
7108
|
+
var jsonSchema = (schema) => record(schema) && !("ast" in schema) && !("~standard" in schema) ? schema : void 0;
|
|
7109
|
+
function normalise(result) {
|
|
7110
|
+
const text2 = typeof result.content === "string" ? result.content : (result.content ?? []).flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
|
|
7111
|
+
return result.output === void 0 ? { text: text2 } : { output: result.output, text: text2 };
|
|
7112
|
+
}
|
|
7113
|
+
function connection(status) {
|
|
7114
|
+
switch (status) {
|
|
7115
|
+
case "connected":
|
|
7116
|
+
return void 0;
|
|
7117
|
+
case "pending":
|
|
7118
|
+
return "CONNECTING";
|
|
7119
|
+
case "needs_auth":
|
|
7120
|
+
return "AUTH_REQUIRED";
|
|
7121
|
+
case "failed":
|
|
7122
|
+
return "CONNECTION_FAILED";
|
|
7123
|
+
case "disabled":
|
|
7124
|
+
return "DISABLED";
|
|
7125
|
+
default:
|
|
7126
|
+
return "NOT_REGISTERED";
|
|
7127
|
+
}
|
|
7128
|
+
}
|
|
7129
|
+
var STATUS_TEXT = {
|
|
7130
|
+
CONNECTING: "is still connecting",
|
|
7131
|
+
AUTH_REQUIRED: "needs a sign-in",
|
|
7132
|
+
CONNECTION_FAILED: "could not connect",
|
|
7133
|
+
DISABLED: "is disabled",
|
|
7134
|
+
NOT_REGISTERED: "is not registered"
|
|
7135
|
+
};
|
|
7136
|
+
async function registerToolsRpc(ctx, options) {
|
|
7137
|
+
const { mcp, owned = [] } = options;
|
|
7138
|
+
const accounts = typeof mcp?.server === "function";
|
|
7139
|
+
if (KNOWN_READS2.length > 0 && !mcp) throw new Error("registerToolsRpc: KNOWN_READS needs the mcp option");
|
|
7140
|
+
if (accounts && NEXT_STEPS.ACCOUNT_UNKNOWN === void 0) throw new Error("registerToolsRpc: per-account servers need NEXT_STEPS.ACCOUNT_UNKNOWN");
|
|
7141
|
+
const names = [...KNOWN_READS2, ...owned.map((method) => method.name)];
|
|
7142
|
+
const duplicate = names.find((name, index) => name === "describe" || names.indexOf(name) !== index);
|
|
7143
|
+
if (duplicate) throw new Error(`registerToolsRpc: the method name "${duplicate}" is taken`);
|
|
7144
|
+
const live = /* @__PURE__ */ new Set();
|
|
7145
|
+
const serverOf = (account2) => {
|
|
7146
|
+
const server = typeof mcp.server === "function" ? mcp.server(account2) : mcp.server;
|
|
7147
|
+
if (server !== void 0) return server;
|
|
7148
|
+
throw new ScriptError({ code: "ACCOUNT_UNKNOWN", what: `there is no account '${account2}'`, next: NEXT_STEPS.ACCOUNT_UNKNOWN });
|
|
7149
|
+
};
|
|
7150
|
+
const session = async (sessionID) => {
|
|
7151
|
+
let info;
|
|
7152
|
+
try {
|
|
7153
|
+
info = await ctx.session.get({ sessionID });
|
|
7154
|
+
} catch (error2) {
|
|
7155
|
+
if (notFound(error2, ["Session.NotFoundError", "SessionNotFoundError"])) throw fail2("SESSION_UNKNOWN", `there is no session '${sessionID}'`);
|
|
7156
|
+
throw error2;
|
|
7157
|
+
}
|
|
7158
|
+
if (!info.agent) throw fail2("SESSION_AGENT_REQUIRED", `session '${sessionID}' names no agent`);
|
|
7159
|
+
let agent;
|
|
7160
|
+
try {
|
|
7161
|
+
agent = await ctx.agent.get({ agentID: info.agent, location: info.location });
|
|
7162
|
+
} catch (error2) {
|
|
7163
|
+
if (notFound(error2, ["Agent.NotFoundError", "AgentNotFoundError"])) throw fail2("SESSION_AGENT_REQUIRED", `session '${sessionID}' names the agent '${info.agent}', which does not exist`);
|
|
7164
|
+
throw error2;
|
|
7165
|
+
}
|
|
7166
|
+
const rules = [...agent.data.permissions, ...info.permissions ?? []];
|
|
7167
|
+
return { agent: info.agent, rules };
|
|
7168
|
+
};
|
|
7169
|
+
const unavailable = async (server, tool, connected) => {
|
|
7170
|
+
const status = (await ctx.mcp.list()).data.find((entry2) => entry2.name === server)?.status.status;
|
|
7171
|
+
const code = connection(status) ?? connected;
|
|
7172
|
+
if (code === "TOOL_UNAVAILABLE") return fail2(code, `OpenCode does not list ${tool} on the connected "${server}" server`);
|
|
7173
|
+
return fail2(code, `the "${server}" server ${STATUS_TEXT[code] ?? "is unavailable"}`);
|
|
7174
|
+
};
|
|
7175
|
+
const denied = (action, error2) => {
|
|
7176
|
+
const reason2 = record(error2) && typeof error2["message"] === "string" ? error2["message"] : String(error2);
|
|
7177
|
+
return fail2("PERMISSION_DENIED", `${action} was refused for this session's agent: ${upstreamText(reason2)}`);
|
|
7178
|
+
};
|
|
7179
|
+
const mcpTarget = (name, envelope2) => {
|
|
7180
|
+
const server = serverOf(envelope2.account);
|
|
7181
|
+
const action = `${server}_${name}`;
|
|
7182
|
+
return {
|
|
7183
|
+
action,
|
|
7184
|
+
mcp: true,
|
|
7185
|
+
async run(call) {
|
|
7186
|
+
if (!mcp.managed(server)) throw fail2("SERVER_CONFLICT", `the "${server}" server in your config is not the one this plugin manages`);
|
|
7187
|
+
const tool = (await ctx.tool.list()).find((entry2) => entry2.id === action);
|
|
7188
|
+
if (!tool) throw await unavailable(server, action, "TOOL_UNAVAILABLE");
|
|
7189
|
+
let result;
|
|
7190
|
+
try {
|
|
7191
|
+
result = await tool.execute(envelope2.input, call);
|
|
7192
|
+
} catch (error2) {
|
|
7193
|
+
if (error2 instanceof Error && error2.message.startsWith(`MCP server "${server}" `)) throw await unavailable(server, action, "CONNECTION_FAILED");
|
|
7194
|
+
throw error2;
|
|
7195
|
+
}
|
|
7196
|
+
return shapeResult(name, normalise(result));
|
|
7197
|
+
}
|
|
7198
|
+
};
|
|
7199
|
+
};
|
|
7200
|
+
const ownedTarget = (spec, envelope2) => ({
|
|
7201
|
+
action: spec.action,
|
|
7202
|
+
mcp: false,
|
|
7203
|
+
run: async (call) => await spec.run(envelope2.input, call) ?? null
|
|
7204
|
+
});
|
|
7205
|
+
const invoke = async (target, raw, context) => {
|
|
7206
|
+
const envelope2 = raw;
|
|
7207
|
+
let method;
|
|
7208
|
+
try {
|
|
7209
|
+
if (context.signal.aborted) throw fail2("CANCELLED", "the call was cancelled");
|
|
7210
|
+
method = target(envelope2);
|
|
7211
|
+
const { agent, rules } = await session(envelope2.sessionID);
|
|
7212
|
+
if (hidden(rules, method.action)) throw fail2("PERMISSION_DENIED", `${method.action} is hidden from this session's agent by a deny rule`);
|
|
7213
|
+
const call = {
|
|
7214
|
+
sessionID: envelope2.sessionID,
|
|
7215
|
+
agent,
|
|
7216
|
+
messageID: MESSAGE_ID,
|
|
7217
|
+
id: `script:${randomUUID()}`,
|
|
7218
|
+
signal: context.signal,
|
|
7219
|
+
progress: async () => {
|
|
7220
|
+
}
|
|
7221
|
+
};
|
|
7222
|
+
live.add(call.id);
|
|
7223
|
+
try {
|
|
7224
|
+
return JSON.parse(JSON.stringify(await method.run(call)));
|
|
7225
|
+
} finally {
|
|
7226
|
+
live.delete(call.id);
|
|
7227
|
+
}
|
|
7228
|
+
} catch (error2) {
|
|
7229
|
+
const failure2 = refusal(error2, method, context.signal);
|
|
7230
|
+
if (!failure2) throw error2;
|
|
7231
|
+
return context.error("failed", new ScriptError(failure2).message, { code: failure2.code, next: failure2.next });
|
|
7232
|
+
}
|
|
7233
|
+
};
|
|
7234
|
+
const refusal = (error2, method, signal) => {
|
|
7235
|
+
if (signal.aborted) return fail2("CANCELLED", "the call was cancelled").failure;
|
|
7236
|
+
if (error2 instanceof ScriptError) return error2.failure;
|
|
7237
|
+
const own2 = pluginError(error2);
|
|
7238
|
+
if (own2 || !method || !toolError(error2)) return own2;
|
|
7239
|
+
if (error2.error !== void 0) return denied(method.action, error2).failure;
|
|
7240
|
+
if (method.mcp) return fail2("UPSTREAM_ERROR", `${method.action} failed: ${upstreamText(error2.message)}`).failure;
|
|
7241
|
+
return void 0;
|
|
7242
|
+
};
|
|
7243
|
+
const describe3 = async () => {
|
|
7244
|
+
const tools = mcp ? await ctx.tool.list() : [];
|
|
7245
|
+
const methods = [];
|
|
7246
|
+
if (mcp) {
|
|
7247
|
+
const server = serverOf(void 0);
|
|
7248
|
+
const managed = mcp.managed(server);
|
|
7249
|
+
const status = managed ? (await ctx.mcp.list()).data.find((entry2) => entry2.name === server)?.status.status : void 0;
|
|
7250
|
+
for (const name of KNOWN_READS2) {
|
|
7251
|
+
const id = `${server}_${name}`;
|
|
7252
|
+
const tool = managed ? tools.find((entry2) => entry2.id === id) : void 0;
|
|
7253
|
+
const reason2 = !managed ? "SERVER_CONFLICT" : tool ? void 0 : connection(status) ?? "TOOL_UNAVAILABLE";
|
|
7254
|
+
const input = jsonSchema(tool?.input) ?? ANY_OBJECT;
|
|
7255
|
+
const output = jsonSchema(tool?.output);
|
|
7256
|
+
methods.push({
|
|
7257
|
+
name,
|
|
7258
|
+
tool: id,
|
|
7259
|
+
kind: "mcp",
|
|
7260
|
+
action: id,
|
|
7261
|
+
description: tool?.description ?? "",
|
|
7262
|
+
available: reason2 === void 0,
|
|
7263
|
+
...reason2 ? { reason: reason2 } : {},
|
|
7264
|
+
input,
|
|
7265
|
+
output: mcpOutput(output)
|
|
7266
|
+
});
|
|
7267
|
+
}
|
|
7268
|
+
}
|
|
7269
|
+
for (const spec of owned) {
|
|
7270
|
+
const reason2 = await spec.available?.();
|
|
7271
|
+
methods.push({
|
|
7272
|
+
name: spec.name,
|
|
7273
|
+
tool: spec.name,
|
|
7274
|
+
kind: "plugin",
|
|
7275
|
+
action: spec.action,
|
|
7276
|
+
description: spec.description,
|
|
7277
|
+
available: reason2 === void 0,
|
|
7278
|
+
...reason2 === void 0 ? {} : { reason: reason2 },
|
|
7279
|
+
input: spec.input,
|
|
7280
|
+
output: spec.output
|
|
7281
|
+
});
|
|
7282
|
+
}
|
|
7283
|
+
return JSON.parse(
|
|
7284
|
+
JSON.stringify({
|
|
7285
|
+
id: TOOLS_RPC_ID,
|
|
7286
|
+
call: `POST /api/rpc/${TOOLS_RPC_ID}/<method> with {"input":{"sessionID":"<session>",${accounts ? `"account":"<account>",` : ""}"input":<the tool's input>}}`,
|
|
7287
|
+
location: ctx.location.directory,
|
|
7288
|
+
methods
|
|
7289
|
+
})
|
|
7290
|
+
);
|
|
7291
|
+
};
|
|
7292
|
+
const envelope = (input, objectInput) => strict(
|
|
7293
|
+
object2({ sessionID: { type: "string", minLength: 1 }, ...accounts ? { account: { type: "string", minLength: 1 } } : {}, input }, ["sessionID", "input"]),
|
|
7294
|
+
(value) => [
|
|
7295
|
+
...typeof value["sessionID"] === "string" && value["sessionID"] !== "" ? [] : [`"sessionID" must be a non-empty string`],
|
|
7296
|
+
...value["account"] === void 0 || typeof value["account"] === "string" && value["account"] !== "" ? [] : [`"account" must be a non-empty string`],
|
|
7297
|
+
...!("input" in value) ? [`"input" is missing`] : objectInput && !plainObject(value["input"]) ? [`"input" must be an object`] : []
|
|
7298
|
+
]
|
|
7299
|
+
);
|
|
7300
|
+
const contract = Rpc2.define({
|
|
7301
|
+
id: TOOLS_RPC_ID,
|
|
7302
|
+
methods: {
|
|
7303
|
+
describe: { input: NO_INPUT, output: DESCRIPTION },
|
|
7304
|
+
...Object.fromEntries(KNOWN_READS2.map((name) => [name, { input: envelope(ANY_OBJECT, true), output: mcpOutput(), errors: { failed: FAILED } }])),
|
|
7305
|
+
...Object.fromEntries(owned.map((spec) => [spec.name, { input: envelope(spec.input, false), output: spec.output, errors: { failed: FAILED } }]))
|
|
7306
|
+
},
|
|
7307
|
+
events: {}
|
|
7308
|
+
});
|
|
7309
|
+
const handlers = {
|
|
7310
|
+
describe: () => describe3(),
|
|
7311
|
+
...Object.fromEntries(KNOWN_READS2.map((name) => [name, (raw, context) => invoke((envelope2) => mcpTarget(name, envelope2), raw, context)])),
|
|
7312
|
+
...Object.fromEntries(owned.map((spec) => [spec.name, (raw, context) => invoke((envelope2) => ownedTarget(spec, envelope2), raw, context)]))
|
|
7313
|
+
};
|
|
7314
|
+
const registration = await ctx.rpc.register(contract, handlers);
|
|
7315
|
+
const hook = await ctx.permission.hook("evaluate", (evaluation) => {
|
|
7316
|
+
if (evaluation.effect !== "ask" || evaluation.source === void 0 || !live.has(evaluation.source.id)) return;
|
|
7317
|
+
evaluation.effect = "deny";
|
|
7318
|
+
evaluation.message = UNATTENDED;
|
|
7319
|
+
});
|
|
7320
|
+
return async () => {
|
|
7321
|
+
await hook.dispose().catch(() => void 0);
|
|
7322
|
+
await registration.dispose().catch(() => void 0);
|
|
7323
|
+
};
|
|
7324
|
+
}
|
|
7325
|
+
|
|
6995
7326
|
// src/index.ts
|
|
6996
7327
|
var src_default = Plugin.define({
|
|
6997
7328
|
id: PLUGIN_ID,
|
|
@@ -7013,6 +7344,7 @@ var src_default = Plugin.define({
|
|
|
7013
7344
|
registrations.push(await registerCommands(ctx, status));
|
|
7014
7345
|
registrations.push(await registerSkills(ctx, new URL("../skills/", import.meta.url)));
|
|
7015
7346
|
registrations.push(await registerRpc(ctx, status));
|
|
7347
|
+
registrations.push({ dispose: await registerToolsRpc(ctx, { mcp: { server: accountServer(options.accounts), managed: () => true } }) });
|
|
7016
7348
|
} catch (error2) {
|
|
7017
7349
|
await cleanup();
|
|
7018
7350
|
throw error2;
|
package/package.json
CHANGED
|
@@ -29,6 +29,23 @@ upload, share or send a recording.
|
|
|
29
29
|
- Load `opencode-plaud-export` before an export, and `opencode-plaud-digest`
|
|
30
30
|
for a summary across several recordings or a follow-up draft.
|
|
31
31
|
|
|
32
|
+
## Scripts
|
|
33
|
+
|
|
34
|
+
When the user wants a job that runs without you, such as a cron job or a
|
|
35
|
+
shell script, it can call Plaud's five read tools through the OpenCode server,
|
|
36
|
+
with no model. The RPC `plaud.recordings.tools` has one method per read, such
|
|
37
|
+
as `list_files`, `get_note` and `get_transcript`, with the tools' own input,
|
|
38
|
+
and `describe`, which lists them with their input schemas. Each call is
|
|
39
|
+
`opencode api post /api/rpc/plaud.recordings.tools/<method>` with the body
|
|
40
|
+
`{"input":{"sessionID":"<session>","input":<the tool's input>}}`; add
|
|
41
|
+
`"account":"work"` to read the `work` account. The script runs as that
|
|
42
|
+
session's agent, under its permission rules, so have the script create its
|
|
43
|
+
session once with `opencode api post /api/session` and a body such as
|
|
44
|
+
`{"title":"weekly recordings","agent":"build"}`. An ask is refused, so the
|
|
45
|
+
agent needs the reads allowed; the default rules allow them. Scripts cannot
|
|
46
|
+
export files, use any other Plaud tool, or get a signed link. The details are
|
|
47
|
+
in the plugin's `docs/scripting.md`.
|
|
48
|
+
|
|
32
49
|
## Data model
|
|
33
50
|
|
|
34
51
|
- **Durations and offsets are milliseconds**, not seconds: `5400000` is
|