@clawling/clawchat-plugin-openclaw 2026.9.10-1 → 2026.9.13-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/dist/src/account-config-writes.js +66 -0
- package/dist/src/account-removal.js +21 -0
- package/dist/src/channel.js +23 -1
- package/dist/src/channel.setup.js +70 -42
- package/dist/src/commands.js +50 -27
- package/dist/src/config.js +83 -13
- package/dist/src/liveware-cli.js +38 -4
- package/dist/src/liveware-sample.js +16 -2
- package/dist/src/llm-context-debug.js +7 -1
- package/dist/src/login.runtime.js +27 -29
- package/dist/src/outbound.js +4 -4
- package/dist/src/permission-result.js +42 -12
- package/dist/src/runtime.js +50 -27
- package/dist/src/terminal-send.js +16 -1
- package/dist/src/tools.js +799 -685
- package/openclaw.plugin.json +150 -0
- package/package.json +1 -1
- package/src/account-config-writes.ts +76 -0
- package/src/account-removal.ts +28 -0
- package/src/channel.setup.ts +77 -43
- package/src/channel.ts +22 -1
- package/src/commands.ts +60 -26
- package/src/config.ts +113 -14
- package/src/liveware-cli.ts +40 -4
- package/src/liveware-sample.ts +20 -2
- package/src/llm-context-debug.ts +7 -1
- package/src/login.runtime.ts +32 -26
- package/src/outbound.ts +4 -4
- package/src/permission-result.ts +46 -11
- package/src/runtime.ts +75 -31
- package/src/terminal-send.ts +16 -1
- package/src/tools.ts +896 -768
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
2
|
+
import { CHANNEL_ID, normalizeOpenclawClawlingAccountId } from "./config.js";
|
|
3
|
+
function readRecord(value) {
|
|
4
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
5
|
+
? value
|
|
6
|
+
: {};
|
|
7
|
+
}
|
|
8
|
+
function findAccountKey(accounts, accountId) {
|
|
9
|
+
return Object.keys(accounts).find((key) => normalizeOpenclawClawlingAccountId(key) === accountId);
|
|
10
|
+
}
|
|
11
|
+
function applyPatch(target, patch) {
|
|
12
|
+
const next = { ...target };
|
|
13
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
14
|
+
if (value === undefined)
|
|
15
|
+
delete next[key];
|
|
16
|
+
else
|
|
17
|
+
next[key] = value;
|
|
18
|
+
}
|
|
19
|
+
return next;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The RAW object a patch for `accountId` lands in (not the merged view — use
|
|
23
|
+
* `readOpenclawClawlingAccountSection` for that). Default: the `accounts.default`
|
|
24
|
+
* entry when the host's single-account promotion created one, else the root.
|
|
25
|
+
* Named: the matching `accounts.<key>` entry (case-insensitive) or `{}`.
|
|
26
|
+
*/
|
|
27
|
+
export function readOpenclawClawlingAccountEntry(cfg, accountId) {
|
|
28
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
29
|
+
const section = readRecord(readRecord(cfg.channels)[CHANNEL_ID]);
|
|
30
|
+
const accounts = readRecord(section.accounts);
|
|
31
|
+
const key = findAccountKey(accounts, id);
|
|
32
|
+
if (key !== undefined)
|
|
33
|
+
return readRecord(accounts[key]);
|
|
34
|
+
return id === DEFAULT_ACCOUNT_ID ? section : {};
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The single config write point. Pure. Default → root section (or
|
|
38
|
+
* `accounts.default` when that entry exists, so a host-promoted token is never
|
|
39
|
+
* shadowed by a stale copy); named → `accounts.<id>` keeping the existing key
|
|
40
|
+
* spelling. A patch value of `undefined` deletes the key. `enabled` is only
|
|
41
|
+
* touched when the patch carries it.
|
|
42
|
+
*/
|
|
43
|
+
export function patchOpenclawClawlingAccountSection(cfg, accountId, patch) {
|
|
44
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
45
|
+
const channels = readRecord(cfg.channels);
|
|
46
|
+
const section = readRecord(channels[CHANNEL_ID]);
|
|
47
|
+
const accounts = readRecord(section.accounts);
|
|
48
|
+
const existingKey = findAccountKey(accounts, id);
|
|
49
|
+
if (id === DEFAULT_ACCOUNT_ID && existingKey === undefined) {
|
|
50
|
+
return {
|
|
51
|
+
...cfg,
|
|
52
|
+
channels: { ...channels, [CHANNEL_ID]: applyPatch(section, patch) },
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
const key = existingKey ?? id;
|
|
56
|
+
return {
|
|
57
|
+
...cfg,
|
|
58
|
+
channels: {
|
|
59
|
+
...channels,
|
|
60
|
+
[CHANNEL_ID]: {
|
|
61
|
+
...section,
|
|
62
|
+
accounts: { ...accounts, [key]: applyPatch(readRecord(accounts[key]), patch) },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { normalizeOpenclawClawlingAccountId } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* `openclaw channels remove --account <id> [--delete]` — the host has already
|
|
4
|
+
* rewritten the config (hybrid adapter) and stopped the account. Blank that
|
|
5
|
+
* account's SQLite credentials so a later start of the same id cannot resume
|
|
6
|
+
* the old identity from the row. Identity columns are kept (re-pair mode).
|
|
7
|
+
*/
|
|
8
|
+
export function clearOpenclawClawlingAccountOnRemoval(params) {
|
|
9
|
+
const accountId = normalizeOpenclawClawlingAccountId(params.accountId);
|
|
10
|
+
if (!params.store) {
|
|
11
|
+
params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite unavailable, nothing to clear`);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
try {
|
|
15
|
+
params.store.clearActivationCredentials({ platform: "openclaw", accountId });
|
|
16
|
+
params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite credentials cleared`);
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
params.log?.(`[${accountId}] clawchat-plugin-openclaw account removed; sqlite clear failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
package/dist/src/channel.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createChatChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
2
2
|
import { createEmptyChannelDirectoryAdapter } from "openclaw/plugin-sdk/directory-runtime";
|
|
3
|
+
import { clearOpenclawClawlingAccountOnRemoval } from "./account-removal.js";
|
|
3
4
|
import { CHANNEL_ID, canStartOpenclawClawlingAccount, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
4
5
|
import { openclawClawlingOutbound } from "./outbound.js";
|
|
5
6
|
import { getOpenclawClawlingRuntime, startOpenclawClawlingGateway } from "./runtime.js";
|
|
7
|
+
import { clawChatDbPathForStateDir, getClawChatStore } from "./storage.js";
|
|
6
8
|
import { openclawClawlingSetupPlugin } from "./channel.setup.js";
|
|
7
9
|
import { getClawChatPlatformPrompt } from "./plugin-prompts.js";
|
|
8
10
|
export const openclawClawlingPlugin = createChatChannelPlugin({
|
|
@@ -23,9 +25,29 @@ export const openclawClawlingPlugin = createChatChannelPlugin({
|
|
|
23
25
|
});
|
|
24
26
|
},
|
|
25
27
|
},
|
|
28
|
+
lifecycle: {
|
|
29
|
+
onAccountRemoved: ({ accountId, runtime }) => {
|
|
30
|
+
let store = null;
|
|
31
|
+
try {
|
|
32
|
+
const stateDir = getOpenclawClawlingRuntime().state?.resolveStateDir?.();
|
|
33
|
+
store = getClawChatStore({
|
|
34
|
+
...(stateDir ? { dbPath: clawChatDbPathForStateDir(stateDir) } : {}),
|
|
35
|
+
log: { error: (message) => runtime.error(message) },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
store = null;
|
|
40
|
+
}
|
|
41
|
+
clearOpenclawClawlingAccountOnRemoval({
|
|
42
|
+
accountId,
|
|
43
|
+
store,
|
|
44
|
+
log: (message) => runtime.log(message),
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
},
|
|
26
48
|
gateway: {
|
|
27
49
|
startAccount: async (ctx) => {
|
|
28
|
-
const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg);
|
|
50
|
+
const account = ctx.account ?? resolveOpenclawClawlingAccount(ctx.cfg, ctx.accountId);
|
|
29
51
|
ctx.log?.info?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle START_ACCOUNT_CALLED configured=${account.configured} enabled=${account.enabled} hasToken=${Boolean(account.token)} hasUserId=${Boolean(account.userId)} websocketUrl=${account.websocketUrl || "(empty)"}`);
|
|
30
52
|
if (!canStartOpenclawClawlingAccount(account)) {
|
|
31
53
|
ctx.log?.error?.(`[${account.accountId}] clawchat-plugin-openclaw lifecycle startAccount refused: websocketUrl is required`);
|
|
@@ -1,68 +1,96 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHybridChannelConfigAdapter } from "openclaw/plugin-sdk/channel-config-helpers";
|
|
2
2
|
import { mutateConfigFile } from "openclaw/plugin-sdk/config-mutation";
|
|
3
|
+
import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
|
|
3
4
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
4
5
|
import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers";
|
|
5
|
-
import { CHANNEL_ID, canStartOpenclawClawlingAccount, listOpenclawClawlingAccountIds, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, } from "./config.js";
|
|
6
|
-
|
|
6
|
+
import { CHANNEL_ID, canStartOpenclawClawlingAccount, listOpenclawClawlingAccountIds, normalizeOpenclawClawlingAccountId, openclawClawlingConfigSchema, resolveOpenclawClawlingAccount, resolveOpenclawClawlingDefaultAccountId, } from "./config.js";
|
|
7
|
+
/** Keys a `channels remove --account default --delete` clears from the section. */
|
|
8
|
+
const CLEAR_BASE_FIELDS = [
|
|
9
|
+
"websocketUrl",
|
|
10
|
+
"baseUrl",
|
|
11
|
+
"token",
|
|
12
|
+
"refreshToken",
|
|
13
|
+
"agentId",
|
|
14
|
+
"userId",
|
|
15
|
+
"ownerUserId",
|
|
16
|
+
"forwardThinking",
|
|
17
|
+
"forwardToolCalls",
|
|
18
|
+
"richInteractions",
|
|
19
|
+
"enabled",
|
|
20
|
+
];
|
|
21
|
+
/**
|
|
22
|
+
* Hybrid layout: the channel root IS the `default` account (and the
|
|
23
|
+
* inheritance base); named accounts live under `accounts.<id>`. The SDK
|
|
24
|
+
* adapter branches `setAccountEnabled` / `deleteAccount` on the id for us.
|
|
25
|
+
*/
|
|
26
|
+
const hybridConfigAdapter = createHybridChannelConfigAdapter({
|
|
7
27
|
sectionKey: CHANNEL_ID,
|
|
8
|
-
resolveAccount: (cfg) => resolveOpenclawClawlingAccount(cfg),
|
|
9
|
-
listAccountIds: () => listOpenclawClawlingAccountIds(),
|
|
10
|
-
defaultAccountId: () =>
|
|
11
|
-
|
|
12
|
-
clearBaseFields: [
|
|
13
|
-
"websocketUrl",
|
|
14
|
-
"baseUrl",
|
|
15
|
-
"token",
|
|
16
|
-
"userId",
|
|
17
|
-
"forwardThinking",
|
|
18
|
-
"forwardToolCalls",
|
|
19
|
-
"richInteractions",
|
|
20
|
-
"enabled",
|
|
21
|
-
],
|
|
28
|
+
resolveAccount: (cfg, accountId) => resolveOpenclawClawlingAccount(cfg, accountId),
|
|
29
|
+
listAccountIds: (cfg) => listOpenclawClawlingAccountIds(cfg),
|
|
30
|
+
defaultAccountId: (cfg) => resolveOpenclawClawlingDefaultAccountId(cfg),
|
|
31
|
+
preserveSectionOnDefaultDelete: true,
|
|
32
|
+
clearBaseFields: [...CLEAR_BASE_FIELDS],
|
|
22
33
|
resolveAllowFrom: (account) => account.allowFrom,
|
|
23
34
|
formatAllowFrom: () => [],
|
|
24
35
|
});
|
|
36
|
+
/** `{ token: undefined, refreshToken: undefined, … }` — `undefined` deletes. */
|
|
37
|
+
const CLEAR_BASE_FIELDS_PATCH = Object.fromEntries(CLEAR_BASE_FIELDS.map((key) => [key, undefined]));
|
|
38
|
+
const configAdapter = {
|
|
39
|
+
...hybridConfigAdapter,
|
|
40
|
+
/**
|
|
41
|
+
* The SDK's default branch clears `clearBaseFields` from the ROOT only. Once
|
|
42
|
+
* the host's single-account promotion has moved the identity into
|
|
43
|
+
* `accounts.default`, that leaves the deleted default fully credentialed:
|
|
44
|
+
* the default reader merges the promoted entry back over the blanked root,
|
|
45
|
+
* `ownerUserId` returns via the token's `oid` claim, and the account resolves
|
|
46
|
+
* `configured: true` again on the next gateway start. Clear the same keys
|
|
47
|
+
* from the promoted entry so `--delete` actually deletes. Named ids are the
|
|
48
|
+
* SDK's alone — it removes the whole `accounts.<id>` entry.
|
|
49
|
+
*/
|
|
50
|
+
deleteAccount: (args) => {
|
|
51
|
+
const next = hybridConfigAdapter.deleteAccount?.(args) ?? args.cfg;
|
|
52
|
+
if (normalizeOpenclawClawlingAccountId(args.accountId) !== DEFAULT_ACCOUNT_ID)
|
|
53
|
+
return next;
|
|
54
|
+
return patchOpenclawClawlingAccountSection(next, DEFAULT_ACCOUNT_ID, CLEAR_BASE_FIELDS_PATCH);
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
function readInviteCode(input) {
|
|
58
|
+
return typeof input.code === "string" && input.code.trim()
|
|
59
|
+
? input.code.trim()
|
|
60
|
+
: typeof input.token === "string"
|
|
61
|
+
? input.token.trim()
|
|
62
|
+
: "";
|
|
63
|
+
}
|
|
25
64
|
/**
|
|
26
65
|
* Invite-code setup adapter used by OpenClaw setup surfaces.
|
|
27
66
|
*
|
|
28
|
-
* `channels add --token
|
|
29
|
-
*
|
|
30
|
-
*
|
|
67
|
+
* `channels add [--account <id>] --token <code>` passes the invite code as
|
|
68
|
+
* setup input and the (optional) account id from the host. The setup write
|
|
69
|
+
* leaves channel config unchanged; `afterAccountConfigWritten` exchanges the
|
|
70
|
+
* invite code and persists credentials for THAT account through the host
|
|
71
|
+
* runtime mutator.
|
|
31
72
|
*/
|
|
32
73
|
const setupAdapter = {
|
|
33
|
-
resolveAccountId: () =>
|
|
34
|
-
validateInput: ({ input }) =>
|
|
35
|
-
const inviteCode = typeof input.code === "string" && input.code.trim()
|
|
36
|
-
? input.code.trim()
|
|
37
|
-
: typeof input.token === "string"
|
|
38
|
-
? input.token.trim()
|
|
39
|
-
: "";
|
|
40
|
-
if (!inviteCode) {
|
|
41
|
-
return "ClawChat invite code is required.";
|
|
42
|
-
}
|
|
43
|
-
return null;
|
|
44
|
-
},
|
|
74
|
+
resolveAccountId: ({ accountId }) => normalizeOpenclawClawlingAccountId(accountId),
|
|
75
|
+
validateInput: ({ input }) => readInviteCode(input) ? null : "ClawChat invite code is required.",
|
|
45
76
|
applyAccountConfig: ({ cfg }) => cfg,
|
|
46
|
-
afterAccountConfigWritten: async ({ cfg, input, runtime }) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
: typeof input.token === "string"
|
|
51
|
-
? input.token.trim()
|
|
52
|
-
: "";
|
|
77
|
+
afterAccountConfigWritten: async ({ cfg, accountId, input, runtime }) => {
|
|
78
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
79
|
+
runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten invoked`);
|
|
80
|
+
const code = readInviteCode(input);
|
|
53
81
|
if (!code) {
|
|
54
|
-
runtime.log(
|
|
82
|
+
runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten skipped: empty invite code`);
|
|
55
83
|
return;
|
|
56
84
|
}
|
|
57
85
|
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
58
86
|
await runOpenclawClawlingLogin({
|
|
59
87
|
cfg,
|
|
60
|
-
accountId:
|
|
88
|
+
accountId: id,
|
|
61
89
|
runtime: { log: (message) => runtime.log(message) },
|
|
62
90
|
readInviteCode: async () => code,
|
|
63
91
|
mutateConfigFile: mutateConfigFile,
|
|
64
92
|
});
|
|
65
|
-
runtime.log(
|
|
93
|
+
runtime.log(`[${id}] clawchat-plugin-openclaw setup afterAccountConfigWritten completed`);
|
|
66
94
|
},
|
|
67
95
|
};
|
|
68
96
|
export const openclawClawlingSetupPlugin = {
|
package/dist/src/commands.js
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { patchOpenclawClawlingAccountSection } from "./account-config-writes.js";
|
|
2
|
+
import { CHANNEL_ID, normalizeOpenclawClawlingAccountId, readOpenclawClawlingAccountSection, } from "./config.js";
|
|
2
3
|
/** Activation intent flags — the two escape hatches out of the live-activation guard. */
|
|
3
4
|
const ACTIVATION_FLAGS = ["--new-account", "--repair"];
|
|
4
5
|
function stripActivationFlags(raw) {
|
|
5
6
|
return ACTIVATION_FLAGS.reduce((acc, flag) => acc.split(flag).join(" "), raw);
|
|
6
7
|
}
|
|
8
|
+
/** `--account <id>` / `--account=<id>`; the value is removed before the code scan so it can never be mistaken for a code. */
|
|
9
|
+
const ACCOUNT_FLAG_PATTERN = /(?:^|\s)--account(?:=|\s+)(\S+)/u;
|
|
10
|
+
/** `--account` with nothing after it — including a dangling `--account=`, which
|
|
11
|
+
* is a typo rather than "use the default account". */
|
|
12
|
+
const BARE_ACCOUNT_FLAG_PATTERN = /(?:^|\s)--account=?(?=\s|$)/u;
|
|
13
|
+
function extractAccountFlag(value) {
|
|
14
|
+
const raw = typeof value === "string" ? value : "";
|
|
15
|
+
const match = ACCOUNT_FLAG_PATTERN.exec(raw);
|
|
16
|
+
if (match) {
|
|
17
|
+
const id = match[1] ?? "";
|
|
18
|
+
if (!id || id.startsWith("--")) {
|
|
19
|
+
return { accountId: null, rest: raw, error: "--account requires an account id" };
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
accountId: normalizeOpenclawClawlingAccountId(id),
|
|
23
|
+
rest: raw.replace(match[0], " "),
|
|
24
|
+
error: null,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (BARE_ACCOUNT_FLAG_PATTERN.test(raw)) {
|
|
28
|
+
return { accountId: null, rest: raw, error: "--account requires an account id" };
|
|
29
|
+
}
|
|
30
|
+
return { accountId: null, rest: raw, error: null };
|
|
31
|
+
}
|
|
7
32
|
/**
|
|
8
33
|
* A whole argument token that looks like a ClawChat connect code.
|
|
9
34
|
*
|
|
@@ -80,27 +105,19 @@ function resolveCommandChatId(ctx) {
|
|
|
80
105
|
? String(ctx.messageThreadId).trim()
|
|
81
106
|
: "");
|
|
82
107
|
}
|
|
83
|
-
function persistOutputVisibility(draft, chatId, outputVisibility) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
108
|
+
function persistOutputVisibility(draft, accountId, chatId, outputVisibility) {
|
|
109
|
+
// Seeded from the MERGED view, never the raw target entry: `mergeAccountConfig`
|
|
110
|
+
// REPLACES `chats` rather than deep-merging it, so a first write seeded from an
|
|
111
|
+
// entry that has no `chats` of its own (a named account, or a `default` the
|
|
112
|
+
// host has promoted into `accounts.default`) would shadow every inherited
|
|
113
|
+
// conversation. Copying the inherited map forward and overriding one key keeps
|
|
114
|
+
// the resolved view additive.
|
|
115
|
+
const merged = readOpenclawClawlingAccountSection(draft, accountId);
|
|
116
|
+
const chats = readRecord(merged.chats);
|
|
87
117
|
const chat = readRecord(chats[chatId]);
|
|
88
|
-
Object.assign(draft, {
|
|
89
|
-
...
|
|
90
|
-
|
|
91
|
-
...channels,
|
|
92
|
-
[CHANNEL_ID]: {
|
|
93
|
-
...channel,
|
|
94
|
-
chats: {
|
|
95
|
-
...chats,
|
|
96
|
-
[chatId]: {
|
|
97
|
-
...chat,
|
|
98
|
-
outputVisibility,
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
});
|
|
118
|
+
Object.assign(draft, patchOpenclawClawlingAccountSection(draft, accountId, {
|
|
119
|
+
chats: { ...chats, [chatId]: { ...chat, outputVisibility } },
|
|
120
|
+
}));
|
|
104
121
|
}
|
|
105
122
|
function formatOutputVisibilityResult(outputVisibility) {
|
|
106
123
|
const detailLevel = {
|
|
@@ -120,21 +137,26 @@ function formatOutputVisibilityResult(outputVisibility) {
|
|
|
120
137
|
export function registerOpenclawClawlingCommands(api) {
|
|
121
138
|
api.registerCommand({
|
|
122
139
|
name: "clawchat-activate",
|
|
123
|
-
description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP.",
|
|
140
|
+
description: "Activate ClawChat with an invite code, e.g. /clawchat-activate K7RM4TQP [--account <id>].",
|
|
124
141
|
acceptsArgs: true,
|
|
125
142
|
requireAuth: true,
|
|
126
143
|
async handler(ctx) {
|
|
127
144
|
const rawArgs = ctx.args ?? ctx.commandBody;
|
|
128
|
-
const
|
|
145
|
+
const accountFlag = extractAccountFlag(rawArgs);
|
|
146
|
+
if (accountFlag.error) {
|
|
147
|
+
return { text: `${accountFlag.error}. Usage: /clawchat-activate K7RM4TQP [--account <id>]` };
|
|
148
|
+
}
|
|
149
|
+
const code = extractInviteCode(accountFlag.rest);
|
|
129
150
|
if (!code) {
|
|
130
|
-
return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP" };
|
|
151
|
+
return { text: "ClawChat invite code is required. Usage: /clawchat-activate K7RM4TQP [--account <id>]" };
|
|
131
152
|
}
|
|
132
|
-
const intent = extractActivationIntent(
|
|
153
|
+
const intent = extractActivationIntent(accountFlag.rest);
|
|
154
|
+
const accountId = accountFlag.accountId ?? normalizeOpenclawClawlingAccountId(ctx.accountId);
|
|
133
155
|
try {
|
|
134
156
|
const { runOpenclawClawlingLogin } = await import("./login.runtime.js");
|
|
135
157
|
await runOpenclawClawlingLogin({
|
|
136
158
|
cfg: ctx.config,
|
|
137
|
-
accountId
|
|
159
|
+
accountId,
|
|
138
160
|
runtime: { log: (message) => api.logger?.info?.(message) },
|
|
139
161
|
readInviteCode: async () => code,
|
|
140
162
|
mutateConfigFile: api.runtime.config.mutateConfigFile,
|
|
@@ -166,10 +188,11 @@ export function registerOpenclawClawlingCommands(api) {
|
|
|
166
188
|
if (!mutateConfigFile) {
|
|
167
189
|
return { text: "OpenClaw runtime config mutation is unavailable for /clawchat-output." };
|
|
168
190
|
}
|
|
191
|
+
const accountId = normalizeOpenclawClawlingAccountId(ctx.accountId);
|
|
169
192
|
await mutateConfigFile({
|
|
170
193
|
afterWrite: { mode: "auto" },
|
|
171
194
|
mutate(draft) {
|
|
172
|
-
persistOutputVisibility(draft, chatId, outputVisibility);
|
|
195
|
+
persistOutputVisibility(draft, accountId, chatId, outputVisibility);
|
|
173
196
|
},
|
|
174
197
|
});
|
|
175
198
|
return { text: formatOutputVisibilityResult(outputVisibility) };
|
package/dist/src/config.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createAccountListHelpers, mergeAccountConfig, normalizeAccountId, resolveNormalizedAccountEntry, } from "openclaw/plugin-sdk/account-core";
|
|
1
2
|
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
2
3
|
export const CHANNEL_ID = "clawchat-plugin-openclaw";
|
|
3
4
|
export const CLAWCHAT_TOKEN_ENV = "CLAWCHAT_TOKEN";
|
|
@@ -40,7 +41,7 @@ export const DEFAULT_ACK = {
|
|
|
40
41
|
// reconnect path re-queues via `queueWhileReconnecting` instead.
|
|
41
42
|
autoResendOnTimeout: false,
|
|
42
43
|
};
|
|
43
|
-
export const
|
|
44
|
+
export const openclawClawlingAccountConfigSchema = {
|
|
44
45
|
type: "object",
|
|
45
46
|
additionalProperties: false,
|
|
46
47
|
properties: {
|
|
@@ -111,6 +112,18 @@ export const openclawClawlingConfigSchema = {
|
|
|
111
112
|
},
|
|
112
113
|
},
|
|
113
114
|
};
|
|
115
|
+
export const openclawClawlingConfigSchema = {
|
|
116
|
+
type: "object",
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
properties: {
|
|
119
|
+
...openclawClawlingAccountConfigSchema.properties,
|
|
120
|
+
defaultAccount: { type: "string" },
|
|
121
|
+
accounts: {
|
|
122
|
+
type: "object",
|
|
123
|
+
additionalProperties: openclawClawlingAccountConfigSchema,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
114
127
|
function isOpenclawClawchatToolAllowEntry(entry) {
|
|
115
128
|
return entry === CHANNEL_ID || entry === "group:plugins";
|
|
116
129
|
}
|
|
@@ -181,6 +194,56 @@ function readChannelSection(cfg) {
|
|
|
181
194
|
const channel = channels[CHANNEL_ID];
|
|
182
195
|
return channel && typeof channel === "object" ? channel : {};
|
|
183
196
|
}
|
|
197
|
+
/** Keys that identify a ClawChat account. Never inherited by a named account from the root. */
|
|
198
|
+
export const ACCOUNT_CREDENTIAL_KEYS = ["token", "refreshToken", "agentId", "userId", "ownerUserId"];
|
|
199
|
+
const ACCOUNT_MAP_KEYS = ["accounts", "defaultAccount"];
|
|
200
|
+
const NESTED_ACCOUNT_OBJECT_KEYS = ["reconnect", "heartbeat", "ack"];
|
|
201
|
+
/** Host-identical account id normalization (lowercase, `[^a-z0-9_-]` → `-`, blank → `default`). */
|
|
202
|
+
export function normalizeOpenclawClawlingAccountId(value) {
|
|
203
|
+
return normalizeAccountId(value);
|
|
204
|
+
}
|
|
205
|
+
function readAccountsMap(section) {
|
|
206
|
+
const accounts = section.accounts;
|
|
207
|
+
return accounts && typeof accounts === "object" && !Array.isArray(accounts)
|
|
208
|
+
? accounts
|
|
209
|
+
: undefined;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* The single config read point. Root = the `default` account and the
|
|
213
|
+
* inheritance base; `accounts.<id>` overrides it. Credential keys are omitted
|
|
214
|
+
* from the base for named ids so an empty `accounts.work: {}` is unconfigured
|
|
215
|
+
* rather than a clone of the default identity. `accounts.default`, when the
|
|
216
|
+
* host's single-account promotion created one, merges over the root.
|
|
217
|
+
*/
|
|
218
|
+
export function readOpenclawClawlingAccountSection(cfg, accountId) {
|
|
219
|
+
const root = readChannelSection(cfg);
|
|
220
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
221
|
+
const resolved = resolveNormalizedAccountEntry(readAccountsMap(root), id, normalizeAccountId);
|
|
222
|
+
// The schema says every entry is an object; the file on disk may disagree
|
|
223
|
+
// (a hand-edited `accounts.work: "work"`). Spreading a non-record merges it
|
|
224
|
+
// in character by character, so treat it as absent.
|
|
225
|
+
const entry = resolved && typeof resolved === "object" && !Array.isArray(resolved) ? resolved : undefined;
|
|
226
|
+
const omitKeys = [...ACCOUNT_MAP_KEYS];
|
|
227
|
+
if (id !== DEFAULT_ACCOUNT_ID)
|
|
228
|
+
omitKeys.push(...ACCOUNT_CREDENTIAL_KEYS);
|
|
229
|
+
return mergeAccountConfig({
|
|
230
|
+
channelConfig: root,
|
|
231
|
+
accountConfig: entry ?? {},
|
|
232
|
+
omitKeys,
|
|
233
|
+
nestedObjectKeys: NESTED_ACCOUNT_OBJECT_KEYS,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const accountListHelpers = createAccountListHelpers(CHANNEL_ID, {
|
|
237
|
+
normalizeAccountId,
|
|
238
|
+
// The root section is always an account, even when unconfigured.
|
|
239
|
+
hasImplicitDefaultAccount: () => true,
|
|
240
|
+
});
|
|
241
|
+
export function listOpenclawClawlingAccountIds(cfg) {
|
|
242
|
+
return accountListHelpers.listAccountIds(cfg);
|
|
243
|
+
}
|
|
244
|
+
export function resolveOpenclawClawlingDefaultAccountId(cfg) {
|
|
245
|
+
return accountListHelpers.resolveDefaultAccountId(cfg);
|
|
246
|
+
}
|
|
184
247
|
function readOptionalString(value) {
|
|
185
248
|
return typeof value === "string" ? value.trim() : "";
|
|
186
249
|
}
|
|
@@ -345,8 +408,16 @@ function readAck(raw) {
|
|
|
345
408
|
: DEFAULT_ACK.autoResendOnTimeout,
|
|
346
409
|
};
|
|
347
410
|
}
|
|
348
|
-
export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
349
|
-
const
|
|
411
|
+
export function resolveOpenclawClawlingAccount(cfg, accountId, env = process.env) {
|
|
412
|
+
const resolvedAccountId = accountId === undefined || accountId === null || !accountId.trim()
|
|
413
|
+
? resolveOpenclawClawlingDefaultAccountId(cfg)
|
|
414
|
+
: normalizeOpenclawClawlingAccountId(accountId);
|
|
415
|
+
const isDefault = resolvedAccountId === DEFAULT_ACCOUNT_ID;
|
|
416
|
+
const root = readChannelSection(cfg);
|
|
417
|
+
const channel = readOpenclawClawlingAccountSection(cfg, resolvedAccountId);
|
|
418
|
+
// Credential env vars are process-wide and can only ever describe ONE
|
|
419
|
+
// identity — they belong to the default account. Endpoint env vars are shared.
|
|
420
|
+
const credentialEnv = isDefault ? env : {};
|
|
350
421
|
// Apply built-in defaults so login/gateway work without prior setup.
|
|
351
422
|
const websocketUrl = readOptionalString(channel.websocketUrl) ||
|
|
352
423
|
readEnvString(env, CLAWCHAT_WEBSOCKET_URL_ENV) ||
|
|
@@ -355,17 +426,19 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
355
426
|
readEnvString(env, CLAWCHAT_BASE_URL_ENV) ||
|
|
356
427
|
DEFAULT_BASE_URL;
|
|
357
428
|
const mediaBaseUrl = readOptionalString(channel.mediaBaseUrl) || readEnvString(env, CLAWCHAT_MEDIA_BASE_URL_ENV);
|
|
358
|
-
const token = readOptionalString(channel.token) || readEnvString(
|
|
359
|
-
const agentId = readOptionalString(channel.agentId) || readEnvString(
|
|
360
|
-
const userId = resolveUserId(token, readOptionalString(channel.userId) || readEnvString(
|
|
429
|
+
const token = readOptionalString(channel.token) || readEnvString(credentialEnv, CLAWCHAT_TOKEN_ENV);
|
|
430
|
+
const agentId = readOptionalString(channel.agentId) || readEnvString(credentialEnv, CLAWCHAT_AGENT_ID_ENV);
|
|
431
|
+
const userId = resolveUserId(token, readOptionalString(channel.userId) || readEnvString(credentialEnv, CLAWCHAT_USER_ID_ENV));
|
|
361
432
|
const ownerUserId = rejectPlaceholderOwnerUserId(readOptionalString(channel.ownerUserId), "channel ownerUserId") ||
|
|
362
|
-
rejectPlaceholderOwnerUserId(readEnvString(
|
|
433
|
+
rejectPlaceholderOwnerUserId(readEnvString(credentialEnv, CLAWCHAT_OWNER_USER_ID_ENV), CLAWCHAT_OWNER_USER_ID_ENV) ||
|
|
363
434
|
// Fall back to the access token's `oid` (owner) claim. A provisioner may
|
|
364
435
|
// inject token + userId but omit ownerUserId; without it the connect gate
|
|
365
436
|
// (hasOpenclawClawlingConnectCredentials) never passes and the plugin sits
|
|
366
437
|
// in the wait-for-activation loop. The owner is always carried in the token.
|
|
367
438
|
decodeJwtStringClaim(token, "oid");
|
|
368
|
-
const
|
|
439
|
+
const rootEnabled = typeof root.enabled === "boolean" ? root.enabled : true;
|
|
440
|
+
const sectionEnabled = typeof channel.enabled === "boolean" ? channel.enabled : true;
|
|
441
|
+
const enabled = rootEnabled && sectionEnabled;
|
|
369
442
|
const outputVisibility = readOutputVisibility(channel.outputVisibility);
|
|
370
443
|
const chats = readChats(channel.chats);
|
|
371
444
|
const groupMode = readGroupMode(channel.groupMode);
|
|
@@ -377,8 +450,8 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
377
450
|
const awarenessNote = typeof channel.awarenessNote === "boolean" ? channel.awarenessNote : false;
|
|
378
451
|
const livewareSample = typeof channel.livewareSample === "boolean" ? channel.livewareSample : true;
|
|
379
452
|
return {
|
|
380
|
-
accountId:
|
|
381
|
-
name: CHANNEL_ID,
|
|
453
|
+
accountId: resolvedAccountId,
|
|
454
|
+
name: isDefault ? CHANNEL_ID : resolvedAccountId,
|
|
382
455
|
enabled,
|
|
383
456
|
configured: hasOpenclawClawlingConnectCredentials({
|
|
384
457
|
websocketUrl,
|
|
@@ -409,6 +482,3 @@ export function resolveOpenclawClawlingAccount(cfg, env = process.env) {
|
|
|
409
482
|
ack: readAck(channel.ack),
|
|
410
483
|
};
|
|
411
484
|
}
|
|
412
|
-
export function listOpenclawClawlingAccountIds() {
|
|
413
|
-
return [DEFAULT_ACCOUNT_ID];
|
|
414
|
-
}
|
package/dist/src/liveware-cli.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
|
|
4
|
+
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/setup";
|
|
5
|
+
import { normalizeOpenclawClawlingAccountId } from "./config.js";
|
|
4
6
|
export const LIVEWARE_BASE_URL = "https://media.clawling.chat/liveware/";
|
|
5
7
|
// A non-default User-Agent is required: the CDN (Cloudflare) returns 403 for
|
|
6
8
|
// known scripting/default UAs, so download requests must identify themselves.
|
|
@@ -11,7 +13,23 @@ export function livewareDir(stateDir) {
|
|
|
11
13
|
return path.join(root, "clawchat", "liveware");
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
|
-
*
|
|
16
|
+
* Root for one account's liveware state: `<stateDir>/clawchat` for the
|
|
17
|
+
* `default` account, `<stateDir>/clawchat/accounts/<id>` for a named one.
|
|
18
|
+
*
|
|
19
|
+
* The `default` account keeps the pre-multi-account layout exactly, so an
|
|
20
|
+
* existing single-account install never has to migrate anything.
|
|
21
|
+
*/
|
|
22
|
+
function livewareAccountRoot(stateDir, accountId) {
|
|
23
|
+
const root = stateDir ?? resolveStateDir();
|
|
24
|
+
const id = normalizeOpenclawClawlingAccountId(accountId);
|
|
25
|
+
return id === DEFAULT_ACCOUNT_ID
|
|
26
|
+
? path.join(root, "clawchat")
|
|
27
|
+
: path.join(root, "clawchat", "accounts", id);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Private HOME for the liveware CLI: `<stateDir>/clawchat/liveware-home` for
|
|
31
|
+
* the default account, `<stateDir>/clawchat/accounts/<id>/liveware-home` for a
|
|
32
|
+
* named one.
|
|
15
33
|
*
|
|
16
34
|
* The CLI keeps its login credentials in `$HOME/.clawling`, which is
|
|
17
35
|
* host-global. Two OpenClaw instances on one box (`openclaw --profile <name>`,
|
|
@@ -22,13 +40,29 @@ export function livewareDir(stateDir) {
|
|
|
22
40
|
* credential store per-instance, which is the same isolation the rest of the
|
|
23
41
|
* plugin already gets for free.
|
|
24
42
|
*
|
|
43
|
+
* Two ACCOUNTS in ONE instance have the same problem and no state dir to tell
|
|
44
|
+
* them apart, so a named account gets its own sub-root: otherwise `work`'s
|
|
45
|
+
* `liveware login` would overwrite `default`'s session in the shared home and
|
|
46
|
+
* `default`'s next `tunnel bind` / `app create` would run as `work`'s user.
|
|
47
|
+
*
|
|
25
48
|
* Migration is automatic: `livewareLogin` runs on every bootstrap AND (best
|
|
26
49
|
* effort) on every relaunch, so an existing install simply re-logs into its new
|
|
27
50
|
* private home on the next start.
|
|
28
51
|
*/
|
|
29
|
-
export function livewareCliHomeDir(stateDir) {
|
|
30
|
-
|
|
31
|
-
|
|
52
|
+
export function livewareCliHomeDir(stateDir, accountId) {
|
|
53
|
+
return path.join(livewareAccountRoot(stateDir, accountId), "liveware-home");
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Where the Liveware Sample app tree is installed for one account:
|
|
57
|
+
* `<stateDir>/clawchat/liveware-sample` (default) or
|
|
58
|
+
* `<stateDir>/clawchat/accounts/<id>/liveware-sample` (named).
|
|
59
|
+
*
|
|
60
|
+
* Per account for the same reason as {@link livewareCliHomeDir}: two
|
|
61
|
+
* supervisors in one process otherwise install, swap and delete inside the
|
|
62
|
+
* same `app/` tree concurrently.
|
|
63
|
+
*/
|
|
64
|
+
export function livewareSampleRootDir(stateDir, accountId) {
|
|
65
|
+
return path.join(livewareAccountRoot(stateDir, accountId), "liveware-sample");
|
|
32
66
|
}
|
|
33
67
|
/**
|
|
34
68
|
* Environment for every liveware CLI invocation: the ambient env with HOME
|