@mnemom/mnemom 0.17.2-next.4 → 0.17.2-next.6
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/commands/agent-slack.d.ts +26 -0
- package/dist/commands/agent-slack.js +212 -0
- package/dist/commands/agent.d.ts +2 -0
- package/dist/commands/agent.js +19 -9
- package/dist/index.d.ts +7 -0
- package/dist/index.js +27 -2
- package/dist/lib/agent.d.ts +10 -0
- package/dist/lib/agent.js +19 -0
- package/dist/lib/launcher-config.d.ts +6 -0
- package/dist/lib/launcher-config.js +49 -0
- package/dist/lib/launcher-store.d.ts +21 -0
- package/dist/lib/launcher-store.js +41 -0
- package/dist/lib/oauth.d.ts +26 -7
- package/dist/lib/oauth.js +53 -14
- package/dist/lib/slack-channel.d.ts +36 -0
- package/dist/lib/slack-channel.js +154 -0
- package/dist/lib/slack-launch.d.ts +15 -0
- package/dist/lib/slack-launch.js +42 -0
- package/dist/lib/slack-route.d.ts +64 -0
- package/dist/lib/slack-route.js +114 -0
- package/dist/lib/slack-router.d.ts +34 -0
- package/dist/lib/slack-router.js +185 -0
- package/dist/lib/slack-state.d.ts +36 -0
- package/dist/lib/slack-state.js +119 -0
- package/dist/lib/slack.d.ts +69 -0
- package/dist/lib/slack.js +178 -0
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent slack …` — let governed sessions talk in Slack, each under its
|
|
3
|
+
* own session name, through ONE Slack app per operator (see lib/slack.ts).
|
|
4
|
+
*
|
|
5
|
+
* mnemom agent slack manifest [app-name] # print the app manifest to paste at api.slack.com
|
|
6
|
+
* mnemom agent slack login # store the bot + app-level tokens (0600)
|
|
7
|
+
* mnemom agent slack logout # remove them
|
|
8
|
+
* mnemom agent slack status # tokens, bot identity, router, live sessions
|
|
9
|
+
* mnemom agent slack post [--channel C] [--thread TS] [--icon-emoji E] <text…|->
|
|
10
|
+
* mnemom agent slack inbox [--peek] # unread messages addressed to THIS session
|
|
11
|
+
* mnemom agent slack listen # this session's channel server (spawned by --slack)
|
|
12
|
+
* mnemom agent slack router # the per-operator Socket Mode router (foreground)
|
|
13
|
+
*
|
|
14
|
+
* The poster name is MNEMOM_AGENT_SESSION_NAME, which the launcher sets in the
|
|
15
|
+
* session's environment — an agent cannot pick another session's name here.
|
|
16
|
+
*/
|
|
17
|
+
/** Entry point for `mnemom agent slack <verb> [args...]`. Throws on misuse. */
|
|
18
|
+
export declare function agentSlackCommand(argv: string[]): Promise<void>;
|
|
19
|
+
interface PostArgs {
|
|
20
|
+
channel?: string;
|
|
21
|
+
thread?: string;
|
|
22
|
+
iconEmoji?: string;
|
|
23
|
+
text: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function parsePostArgs(argv: string[]): PostArgs;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mnemom agent slack …` — let governed sessions talk in Slack, each under its
|
|
3
|
+
* own session name, through ONE Slack app per operator (see lib/slack.ts).
|
|
4
|
+
*
|
|
5
|
+
* mnemom agent slack manifest [app-name] # print the app manifest to paste at api.slack.com
|
|
6
|
+
* mnemom agent slack login # store the bot + app-level tokens (0600)
|
|
7
|
+
* mnemom agent slack logout # remove them
|
|
8
|
+
* mnemom agent slack status # tokens, bot identity, router, live sessions
|
|
9
|
+
* mnemom agent slack post [--channel C] [--thread TS] [--icon-emoji E] <text…|->
|
|
10
|
+
* mnemom agent slack inbox [--peek] # unread messages addressed to THIS session
|
|
11
|
+
* mnemom agent slack listen # this session's channel server (spawned by --slack)
|
|
12
|
+
* mnemom agent slack router # the per-operator Socket Mode router (foreground)
|
|
13
|
+
*
|
|
14
|
+
* The poster name is MNEMOM_AGENT_SESSION_NAME, which the launcher sets in the
|
|
15
|
+
* session's environment — an agent cannot pick another session's name here.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import { configKeySpec, loadLauncherConfig, writeLauncherConfig } from "../lib/launcher-config.js";
|
|
20
|
+
import { runChannelServer } from "../lib/slack-channel.js";
|
|
21
|
+
import { mergeStore, readStore } from "../lib/launcher-store.js";
|
|
22
|
+
import { askInput, isInteractive } from "../lib/prompt.js";
|
|
23
|
+
import { explainSlackError, looksLikeAppToken, looksLikeBotToken, postMessage, resolveSlackToken, sessionPosterName, slackAppManifest, slackCall, SLACK_APP_TOKEN_STORE_KEY, SLACK_TOKEN_STORE_KEY, } from "../lib/slack.js";
|
|
24
|
+
import { routerStatus, runRouter } from "../lib/slack-router.js";
|
|
25
|
+
import { normSession } from "../lib/slack-route.js";
|
|
26
|
+
import { liveSessions, readInboxFrom, recordPost, slackDir } from "../lib/slack-state.js";
|
|
27
|
+
const out = (line = "") => console.log(line);
|
|
28
|
+
/** Entry point for `mnemom agent slack <verb> [args...]`. Throws on misuse. */
|
|
29
|
+
export async function agentSlackCommand(argv) {
|
|
30
|
+
const [verb, ...rest] = argv;
|
|
31
|
+
switch (verb) {
|
|
32
|
+
case "manifest":
|
|
33
|
+
// The manifest holds scope NAMES, never a credential; written to stdout (not logged)
|
|
34
|
+
// so it can be piped straight into Slack's "create from manifest" box.
|
|
35
|
+
process.stdout.write(JSON.stringify(slackAppManifest(rest.join(" ") || defaultAppName()), null, 2) + "\n");
|
|
36
|
+
return;
|
|
37
|
+
case "login":
|
|
38
|
+
return login();
|
|
39
|
+
case "logout":
|
|
40
|
+
mergeStore({ [SLACK_TOKEN_STORE_KEY]: null, [SLACK_APP_TOKEN_STORE_KEY]: null });
|
|
41
|
+
out("mnemom agent slack: tokens removed from ~/.mnemom/launcher.json.");
|
|
42
|
+
return;
|
|
43
|
+
case undefined:
|
|
44
|
+
case "status":
|
|
45
|
+
return status();
|
|
46
|
+
case "post":
|
|
47
|
+
return post(rest);
|
|
48
|
+
case "inbox":
|
|
49
|
+
return inbox(rest);
|
|
50
|
+
case "router":
|
|
51
|
+
return router();
|
|
52
|
+
case "listen":
|
|
53
|
+
return listen();
|
|
54
|
+
default:
|
|
55
|
+
throw new Error(`mnemom agent slack: unknown verb '${verb}'. Use manifest | login | logout | status | post | inbox | listen | router.`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function defaultAppName() {
|
|
59
|
+
const user = (process.env.USER ?? "").replace(/[^A-Za-z0-9-]/g, "");
|
|
60
|
+
return user ? `${user}-agents` : "mnemom-agents";
|
|
61
|
+
}
|
|
62
|
+
function botToken() {
|
|
63
|
+
const { token } = resolveSlackToken(process.env, readStore(), "bot");
|
|
64
|
+
if (!token) {
|
|
65
|
+
throw new Error("mnemom agent slack: no bot token — run `mnemom agent slack login` (or set MNEMOM_SLACK_BOT_TOKEN).");
|
|
66
|
+
}
|
|
67
|
+
return token;
|
|
68
|
+
}
|
|
69
|
+
async function login() {
|
|
70
|
+
if (!isInteractive()) {
|
|
71
|
+
throw new Error("mnemom agent slack login: needs a terminal (tokens are read masked, never from argv).");
|
|
72
|
+
}
|
|
73
|
+
const bot = (await askInput("Bot User OAuth Token (xoxb-…): ", true)).trim();
|
|
74
|
+
if (!looksLikeBotToken(bot))
|
|
75
|
+
throw new Error("that is not a bot token (expected xoxb-…).");
|
|
76
|
+
const auth = await slackCall("auth.test", bot, {});
|
|
77
|
+
if (!auth.ok)
|
|
78
|
+
throw new Error(explainSlackError(auth.error));
|
|
79
|
+
const app = (await askInput("App-Level Token with connections:write (xapp-…, Enter to skip): ", true)).trim();
|
|
80
|
+
if (app && !looksLikeAppToken(app))
|
|
81
|
+
throw new Error("that is not an app-level token (expected xapp-…).");
|
|
82
|
+
mergeStore({
|
|
83
|
+
[SLACK_TOKEN_STORE_KEY]: bot,
|
|
84
|
+
...(app ? { [SLACK_APP_TOKEN_STORE_KEY]: app } : {}),
|
|
85
|
+
});
|
|
86
|
+
const cfg = loadLauncherConfig();
|
|
87
|
+
if (!cfg.slackAllow?.length) {
|
|
88
|
+
const me = (await askInput("Your Slack member ID — only allowlisted members can reach a session (U…): ")).trim();
|
|
89
|
+
if (me) {
|
|
90
|
+
configKeySpec("slack_allow")?.set(cfg, me);
|
|
91
|
+
writeLauncherConfig(cfg);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
out(`mnemom agent slack: saved. Bot ${String(auth.user)} in ${String(auth.team)}.`);
|
|
95
|
+
if (!app)
|
|
96
|
+
out(" (No app-level token: sessions can post, but cannot receive replies until you add one.)");
|
|
97
|
+
out(" Invite the bot to a channel with /invite @" + String(auth.user) + " before posting there.");
|
|
98
|
+
}
|
|
99
|
+
async function status() {
|
|
100
|
+
const store = readStore();
|
|
101
|
+
const bot = resolveSlackToken(process.env, store, "bot");
|
|
102
|
+
const app = resolveSlackToken(process.env, store, "app");
|
|
103
|
+
out(`bot token: ${bot.source ?? "not set"}`);
|
|
104
|
+
out(`app token: ${app.source ?? "not set"}`);
|
|
105
|
+
if (bot.token) {
|
|
106
|
+
const a = await slackCall("auth.test", bot.token, {}).catch((e) => ({
|
|
107
|
+
ok: false,
|
|
108
|
+
error: e.message,
|
|
109
|
+
}));
|
|
110
|
+
out(`bot identity: ${a.ok ? `${String(a.user)} @ ${String(a.team)}` : explainSlackError(a.error)}`);
|
|
111
|
+
}
|
|
112
|
+
out(`default channel: ${loadLauncherConfig().slackChannel ?? "(none — pass --channel)"}`);
|
|
113
|
+
out(`this session: ${sessionPosterName(process.env) ?? "(not inside a `mnemom agent` session)"}`);
|
|
114
|
+
const r = routerStatus();
|
|
115
|
+
out(`router: ${r.running ? `running (pid ${r.pid})` : "not running"}`);
|
|
116
|
+
const live = liveSessions();
|
|
117
|
+
out(`listening: ${live.length ? live.join(", ") : "none"}`);
|
|
118
|
+
const allow = loadLauncherConfig().slackAllow ?? [];
|
|
119
|
+
out(`allowed senders: ${allow.length ? allow.join(", ") : "none — no message will reach a session"}`);
|
|
120
|
+
}
|
|
121
|
+
export function parsePostArgs(argv) {
|
|
122
|
+
const res = { text: "" };
|
|
123
|
+
const words = [];
|
|
124
|
+
for (let i = 0; i < argv.length; i++) {
|
|
125
|
+
const a = argv[i];
|
|
126
|
+
const val = () => {
|
|
127
|
+
const v = argv[++i];
|
|
128
|
+
if (v === undefined)
|
|
129
|
+
throw new Error(`mnemom agent slack post: ${a} needs a value.`);
|
|
130
|
+
return v;
|
|
131
|
+
};
|
|
132
|
+
if (a === "--channel" || a === "-c")
|
|
133
|
+
res.channel = val();
|
|
134
|
+
else if (a === "--thread" || a === "-t")
|
|
135
|
+
res.thread = val();
|
|
136
|
+
else if (a === "--icon-emoji")
|
|
137
|
+
res.iconEmoji = val();
|
|
138
|
+
else if (a === "--") {
|
|
139
|
+
words.push(...argv.slice(i + 1));
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
else
|
|
143
|
+
words.push(a);
|
|
144
|
+
}
|
|
145
|
+
res.text = words.join(" ");
|
|
146
|
+
return res;
|
|
147
|
+
}
|
|
148
|
+
async function post(argv) {
|
|
149
|
+
const args = parsePostArgs(argv);
|
|
150
|
+
let text = args.text;
|
|
151
|
+
if (text === "-" || (!text && !process.stdin.isTTY))
|
|
152
|
+
text = readFileSync(0, "utf8");
|
|
153
|
+
text = text.trim();
|
|
154
|
+
if (!text)
|
|
155
|
+
throw new Error("mnemom agent slack post: nothing to post.");
|
|
156
|
+
const channel = args.channel ?? loadLauncherConfig().slackChannel;
|
|
157
|
+
if (!channel) {
|
|
158
|
+
throw new Error("mnemom agent slack post: no channel — pass --channel or `mnemom agent config set slack_channel <#name>`.");
|
|
159
|
+
}
|
|
160
|
+
const session = sessionPosterName(process.env);
|
|
161
|
+
const res = await postMessage({
|
|
162
|
+
token: botToken(),
|
|
163
|
+
channel,
|
|
164
|
+
text,
|
|
165
|
+
username: session,
|
|
166
|
+
iconEmoji: args.iconEmoji,
|
|
167
|
+
threadTs: args.thread,
|
|
168
|
+
});
|
|
169
|
+
if (session)
|
|
170
|
+
recordPost({ session, channel: res.channel, ts: res.ts, thread_ts: args.thread });
|
|
171
|
+
out(JSON.stringify({ ok: true, channel: res.channel, ts: res.ts, as: session ?? null }));
|
|
172
|
+
}
|
|
173
|
+
// ── inbox (pull) ─────────────────────────────────────────────────────────────
|
|
174
|
+
// Normalised like the inbox file name, so a session name can never escape offsets/.
|
|
175
|
+
const offsetFile = (session) => join(slackDir(), "offsets", `${normSession(session)}.off`);
|
|
176
|
+
function readOffset(session) {
|
|
177
|
+
try {
|
|
178
|
+
return Number(readFileSync(offsetFile(session), "utf8")) || 0;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Print (and, unless --peek, consume) the messages routed to this session. */
|
|
185
|
+
async function inbox(argv) {
|
|
186
|
+
const session = sessionPosterName(process.env);
|
|
187
|
+
if (!session)
|
|
188
|
+
throw new Error("mnemom agent slack inbox: not inside a `mnemom agent` session.");
|
|
189
|
+
const { messages, offset } = readInboxFrom(session, readOffset(session));
|
|
190
|
+
if (!argv.includes("--peek")) {
|
|
191
|
+
mkdirSync(join(slackDir(), "offsets"), { recursive: true, mode: 0o700 });
|
|
192
|
+
writeFileSync(offsetFile(session), String(offset), { mode: 0o600 });
|
|
193
|
+
}
|
|
194
|
+
out(JSON.stringify(messages));
|
|
195
|
+
}
|
|
196
|
+
async function router() {
|
|
197
|
+
const store = readStore();
|
|
198
|
+
const app = resolveSlackToken(process.env, store, "app").token;
|
|
199
|
+
if (!app)
|
|
200
|
+
throw new Error("mnemom agent slack router: no app-level token — run `mnemom agent slack login`.");
|
|
201
|
+
await runRouter({
|
|
202
|
+
botToken: botToken(),
|
|
203
|
+
appToken: app,
|
|
204
|
+
allow: loadLauncherConfig().slackAllow ?? [],
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
async function listen() {
|
|
208
|
+
const session = sessionPosterName(process.env);
|
|
209
|
+
if (!session)
|
|
210
|
+
throw new Error("mnemom agent slack listen: not inside a `mnemom agent` session.");
|
|
211
|
+
await runChannelServer({ session, botToken: botToken() });
|
|
212
|
+
}
|
package/dist/commands/agent.d.ts
CHANGED
|
@@ -60,6 +60,8 @@ export interface AgentOptions {
|
|
|
60
60
|
context?: boolean;
|
|
61
61
|
/** context-hint seed default: commander `--no-context-hint` sets this false. */
|
|
62
62
|
contextHint?: boolean;
|
|
63
|
+
/** Attach the Slack channel server (`--slack`; lib/slack-launch.ts). */
|
|
64
|
+
slack?: boolean;
|
|
63
65
|
/**
|
|
64
66
|
* EXPERIMENTAL, off by default (operator directive, MNE-7564): registers the `handoff` MCP
|
|
65
67
|
* tool and tells the gateway `CONTEXT_HANDOFF_ENABLED=true` for this session only. Absent/false
|
package/dist/commands/agent.js
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* an argv value.
|
|
27
27
|
*/
|
|
28
28
|
import { spawn, spawnSync } from "node:child_process";
|
|
29
|
-
import { existsSync,
|
|
29
|
+
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync, renameSync, } from "node:fs";
|
|
30
30
|
import { randomUUID } from "node:crypto";
|
|
31
31
|
import { tmpdir, homedir } from "node:os";
|
|
32
32
|
import { basename, join, dirname } from "node:path";
|
|
@@ -39,9 +39,11 @@ import { fmt } from "../lib/format.js";
|
|
|
39
39
|
import { askInput, askYesNo, isInteractive } from "../lib/prompt.js";
|
|
40
40
|
import { goalAlignmentLine, resolveSessionSetup } from "./agent-session.js";
|
|
41
41
|
import { defaultAgentSlug, ensureGovernedAgent, ensureSessionAndBalance, isQuietPreambleExit, resolveDefaultSlug, resolvePreflightedAccount, } from "./agent-setup.js";
|
|
42
|
-
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfigB64, resolveContextFold, resolveContextHandoff, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, sanitizeAgentName, } from "../lib/agent.js";
|
|
42
|
+
import { assembleContract, buildGoalModeHeader, buildCustomHeaders, buildHandoffMcpConfigJson, buildSessionConfigB64, resolveContextFold, resolveContextHandoff, resolveHandoffGatewayHost, HANDOFF_RC_KEY_ENV_NAME, isProdGateway, launchPreferenceExplicit, launchPreflighted, remoteControlAllowed, resolveAnthropicDoor, resolveConversationId, resolveGatewayHost, resolveLaunchShape, resolveRemoteControlShape, passthroughName, sanitizeAgentName, sessionDisplayName, SESSION_NAME_ENV, } from "../lib/agent.js";
|
|
43
|
+
import { LAUNCHER_STORE_PATH, mergeStore } from "../lib/launcher-store.js";
|
|
44
|
+
import { slackChannelArgs } from "../lib/slack-launch.js";
|
|
43
45
|
/** The CLI's own provider-key store (zero 1Password): ~/.mnemom/launcher.json, 0600. */
|
|
44
|
-
const CODE_STORE =
|
|
46
|
+
const CODE_STORE = LAUNCHER_STORE_PATH;
|
|
45
47
|
const say = (line = "") => console.error(line);
|
|
46
48
|
/** Entry point for `mnemom agent [scenario]`. Throws on fatal misconfig. */
|
|
47
49
|
export async function agentCommand(scenarioArg, options = {}) {
|
|
@@ -306,6 +308,7 @@ export async function agentCommand(scenarioArg, options = {}) {
|
|
|
306
308
|
door,
|
|
307
309
|
anthropicKey,
|
|
308
310
|
agent,
|
|
311
|
+
scenario,
|
|
309
312
|
conversationId,
|
|
310
313
|
envelope,
|
|
311
314
|
goalMode,
|
|
@@ -445,13 +448,10 @@ function readStoredKey() {
|
|
|
445
448
|
return undefined;
|
|
446
449
|
}
|
|
447
450
|
}
|
|
448
|
-
/** Persist the provider key to ~/.mnemom/launcher.json at 0600 (atomic). Best-effort. */
|
|
451
|
+
/** Persist the provider key to ~/.mnemom/launcher.json at 0600 (atomic, merged). Best-effort. */
|
|
449
452
|
function storeKey(key) {
|
|
450
453
|
try {
|
|
451
|
-
|
|
452
|
-
const tmp = join(MNEMOM_DIR, `.code-tmp-${process.pid}-${Date.now()}.json`);
|
|
453
|
-
writeFileSync(tmp, JSON.stringify({ anthropic_key: key }, null, 2), { mode: 0o600 });
|
|
454
|
-
renameSync(tmp, CODE_STORE);
|
|
454
|
+
mergeStore({ anthropic_key: key }, CODE_STORE);
|
|
455
455
|
}
|
|
456
456
|
catch {
|
|
457
457
|
// best effort — the launch proceeds with the in-memory key either way
|
|
@@ -632,6 +632,10 @@ function launchTerminal(ctx) {
|
|
|
632
632
|
childEnv.ANTHROPIC_BASE_URL = ctx.door;
|
|
633
633
|
childEnv.ANTHROPIC_AUTH_TOKEN = "mnemom-gateway";
|
|
634
634
|
childEnv.ANTHROPIC_CUSTOM_HEADERS = headers;
|
|
635
|
+
const sessionName = passthroughName(ctx.options.passthrough ?? []) ?? sessionDisplayName(ctx.agent, ctx.scenario);
|
|
636
|
+
childEnv[SESSION_NAME_ENV] = sessionName;
|
|
637
|
+
if (ctx.options.slack)
|
|
638
|
+
args.push(...slackChannelArgs(sessionName));
|
|
635
639
|
if (ctx.contextHint) {
|
|
636
640
|
seedContextHintFlag(childEnv);
|
|
637
641
|
childEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC = "1";
|
|
@@ -752,7 +756,7 @@ function spawnRcCli(ctx) {
|
|
|
752
756
|
// Session name for the claude.ai session list: "<agent>-<scenario>", unless
|
|
753
757
|
// the caller passed their own `--name <x>` after `--` (lifted out here).
|
|
754
758
|
const rawPassthrough = ctx.options.passthrough ?? [];
|
|
755
|
-
let rcName =
|
|
759
|
+
let rcName = sessionDisplayName(ctx.agent, ctx.scenario);
|
|
756
760
|
const passthrough = [];
|
|
757
761
|
for (let i = 0; i < rawPassthrough.length; i++) {
|
|
758
762
|
if (rawPassthrough[i] === "--name" && i + 1 < rawPassthrough.length) {
|
|
@@ -786,6 +790,7 @@ function spawnRcCli(ctx) {
|
|
|
786
790
|
childEnv.HTTPS_PROXY = `http://127.0.0.1:${ctx.port}`;
|
|
787
791
|
childEnv.https_proxy = `http://127.0.0.1:${ctx.port}`;
|
|
788
792
|
childEnv.NODE_EXTRA_CA_CERTS = ctx.ca;
|
|
793
|
+
childEnv[SESSION_NAME_ENV] = rcName;
|
|
789
794
|
// Context handoff (MNE-7564), RC shapes only: the strip list above deliberately removes every
|
|
790
795
|
// ANTHROPIC_* var, so the handoff MCP server has nothing to inherit for the key unless we give
|
|
791
796
|
// it something — a dedicated, non-ANTHROPIC-named var, added AFTER the strip loop so it can
|
|
@@ -802,6 +807,9 @@ function spawnRcCli(ctx) {
|
|
|
802
807
|
warnServerDrops(ctx.options, ctx.yolo);
|
|
803
808
|
if (ctx.options.model)
|
|
804
809
|
childEnv.ANTHROPIC_MODEL = ctx.options.model;
|
|
810
|
+
if (ctx.options.slack) {
|
|
811
|
+
say("mnemom agent: --slack is not applied to a headless server (a channel server needs an interactive confirmation); use the terminal or remote-control shape.");
|
|
812
|
+
}
|
|
805
813
|
args = ["remote-control", "--name", rcName, ...passthrough];
|
|
806
814
|
// Additive, never `--strict-mcp-config`: --trim-mcp is not applied under --server (every
|
|
807
815
|
// dispatched session keeps its full MCP set, unrestricted — see warnServerDrops below), so
|
|
@@ -821,6 +829,8 @@ function spawnRcCli(ctx) {
|
|
|
821
829
|
handoffMcpConfigPath: ctx.handoffMcpConfigPath,
|
|
822
830
|
});
|
|
823
831
|
args = ["--remote-control", rcName, ...flagArgs];
|
|
832
|
+
if (ctx.options.slack)
|
|
833
|
+
args.push(...slackChannelArgs(rcName));
|
|
824
834
|
say(`mnemom agent: remote-control: interactive session in THIS terminal with Remote Control on (session name '${rcName}'); it also appears under Remote Control at claude.ai/code and in the Claude app. Exit the CLI to stop everything.`);
|
|
825
835
|
}
|
|
826
836
|
const child = spawn(ctx.cli.bin, args, { stdio: "inherit", env: childEnv });
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { program } from "commander";
|
|
3
3
|
export { program };
|
|
4
|
+
/**
|
|
5
|
+
* The entrypoint dispatch. `mnemom agent slack …` owns its own flags (--thread,
|
|
6
|
+
* --channel, …), which the `agent` launcher options would otherwise reject as
|
|
7
|
+
* unknown — so it bypasses commander entirely and reaches the sub-verb parser
|
|
8
|
+
* verbatim. Exported (not auto-run) so the bypass is unit-testable.
|
|
9
|
+
*/
|
|
10
|
+
export declare function runEntrypoint(argv?: string[]): void;
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { listenCommand } from "./commands/listen.js";
|
|
|
24
24
|
import { tryMeCommand } from "./commands/try-me.js";
|
|
25
25
|
import { agentCommand } from "./commands/agent.js";
|
|
26
26
|
import { agentConfigCommand } from "./commands/agent-config.js";
|
|
27
|
+
import { agentSlackCommand } from "./commands/agent-slack.js";
|
|
27
28
|
import { agentDoctorCommand } from "./commands/agent-doctor.js";
|
|
28
29
|
import { agentSetupCommand } from "./commands/agent-setup.js";
|
|
29
30
|
import { agentInviteCommand, runAgentInvocation } from "./commands/agent-invite.js";
|
|
@@ -135,6 +136,7 @@ const agentCmd = program
|
|
|
135
136
|
.option("--no-setup", "Skip governed-agent provisioning entirely for this launch")
|
|
136
137
|
.option("--no-yolo", "Do NOT pass --dangerously-skip-permissions (yolo is on by default)")
|
|
137
138
|
.option("--no-context-hint", "Do NOT seed Claude Code's context-hint feature flag before launch")
|
|
139
|
+
.option("--slack", "Attach the Slack channel: messages addressed to this session arrive in it, and it can reply (see `mnemom agent slack`)")
|
|
138
140
|
.option("--context", "Force context folding ON for this launch even when launcher.toml (context = false) or MNEMOM_AGENT_CONTEXT turns it off")
|
|
139
141
|
.option("--no-context", "Disable context folding for this launch (folding is on by default; tells the gateway to skip it for this session)")
|
|
140
142
|
.option("--context-handoff", "EXPERIMENTAL, off by default: register the `handoff(note)` MCP tool and tell the gateway to turn on the context-handoff capability for this session")
|
|
@@ -162,6 +164,12 @@ const agentCmd = program
|
|
|
162
164
|
await agentConfigCommand(passthrough);
|
|
163
165
|
return;
|
|
164
166
|
}
|
|
167
|
+
// Sub-verb: `mnemom agent slack …` posts/receives Slack messages as this
|
|
168
|
+
// session (lib/slack.ts) and never launches.
|
|
169
|
+
if (scenario === "slack") {
|
|
170
|
+
await agentSlackCommand(passthrough);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
165
173
|
if (opts.gateway)
|
|
166
174
|
process.env.MNEMOM_AGENT_GATEWAY = opts.gateway;
|
|
167
175
|
// Persistent settings (~/.mnemom/launcher.toml) sit UNDER env and flags:
|
|
@@ -220,6 +228,7 @@ const agentCmd = program
|
|
|
220
228
|
trimMcp: opts.trimMcp,
|
|
221
229
|
yolo: opts.yolo,
|
|
222
230
|
contextHint: opts.contextHint,
|
|
231
|
+
slack: opts.slack ?? cfg.slackListen,
|
|
223
232
|
context: opts.context,
|
|
224
233
|
contextHandoff: opts.contextHandoff,
|
|
225
234
|
setup: opts.setup,
|
|
@@ -1874,6 +1883,22 @@ export { program };
|
|
|
1874
1883
|
// Parse argv only when invoked as the CLI entrypoint — not when imported.
|
|
1875
1884
|
// The symlink-safe guard lives in ./lib/entrypoint so it can be unit-tested
|
|
1876
1885
|
// without executing this module's top-level parse().
|
|
1877
|
-
|
|
1878
|
-
|
|
1886
|
+
/**
|
|
1887
|
+
* The entrypoint dispatch. `mnemom agent slack …` owns its own flags (--thread,
|
|
1888
|
+
* --channel, …), which the `agent` launcher options would otherwise reject as
|
|
1889
|
+
* unknown — so it bypasses commander entirely and reaches the sub-verb parser
|
|
1890
|
+
* verbatim. Exported (not auto-run) so the bypass is unit-testable.
|
|
1891
|
+
*/
|
|
1892
|
+
export function runEntrypoint(argv = process.argv.slice(2)) {
|
|
1893
|
+
if (argv[0] === "agent" && argv[1] === "slack") {
|
|
1894
|
+
agentSlackCommand(argv.slice(2)).catch((err) => {
|
|
1895
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1896
|
+
process.exit(1);
|
|
1897
|
+
});
|
|
1898
|
+
}
|
|
1899
|
+
else {
|
|
1900
|
+
program.parse();
|
|
1901
|
+
}
|
|
1879
1902
|
}
|
|
1903
|
+
if (isEntrypoint(import.meta.url, process.argv[1]))
|
|
1904
|
+
runEntrypoint();
|
package/dist/lib/agent.d.ts
CHANGED
|
@@ -92,6 +92,16 @@ export declare function resolveHandoffGatewayHost(door: string): string;
|
|
|
92
92
|
* survives (the caller rejects that).
|
|
93
93
|
*/
|
|
94
94
|
export declare function sanitizeAgentName(raw: string): string;
|
|
95
|
+
/**
|
|
96
|
+
* The env var the launcher sets in the child CLI's environment to the session's
|
|
97
|
+
* display name, so tools running INSIDE the session (e.g. `mnemom agent slack
|
|
98
|
+
* post`) can identify themselves by the same name the claude.ai session list shows.
|
|
99
|
+
*/
|
|
100
|
+
export declare const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
101
|
+
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
102
|
+
export declare function sessionDisplayName(agent: string, scenario: string): string;
|
|
103
|
+
/** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
|
|
104
|
+
export declare function passthroughName(passthrough: readonly string[]): string | undefined;
|
|
95
105
|
/** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
|
|
96
106
|
export declare function resolveConversationId(raw: string | undefined): string;
|
|
97
107
|
/** A strictly-positive integer (rejects 0, negatives, decimals, NaN, non-numeric strings). */
|
package/dist/lib/agent.js
CHANGED
|
@@ -74,6 +74,25 @@ export function sanitizeAgentName(raw) {
|
|
|
74
74
|
.replace(/^-+/, "")
|
|
75
75
|
.replace(/-+$/, "");
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* The env var the launcher sets in the child CLI's environment to the session's
|
|
79
|
+
* display name, so tools running INSIDE the session (e.g. `mnemom agent slack
|
|
80
|
+
* post`) can identify themselves by the same name the claude.ai session list shows.
|
|
81
|
+
*/
|
|
82
|
+
export const SESSION_NAME_ENV = "MNEMOM_AGENT_SESSION_NAME";
|
|
83
|
+
/** The session display name: "<agent>-<scenario>" (sanitised), else the agent slug. */
|
|
84
|
+
export function sessionDisplayName(agent, scenario) {
|
|
85
|
+
return sanitizeAgentName(`${agent}-${scenario}`) || agent;
|
|
86
|
+
}
|
|
87
|
+
/** A caller's own `--name <x>` in the passthrough args, if any (last one wins). */
|
|
88
|
+
export function passthroughName(passthrough) {
|
|
89
|
+
let name;
|
|
90
|
+
for (let i = 0; i < passthrough.length - 1; i++) {
|
|
91
|
+
if (passthrough[i] === "--name")
|
|
92
|
+
name = passthrough[i + 1];
|
|
93
|
+
}
|
|
94
|
+
return name;
|
|
95
|
+
}
|
|
77
96
|
/** A single-line, header-safe conversation id: honour a caller value, else a uuid. */
|
|
78
97
|
export function resolveConversationId(raw) {
|
|
79
98
|
const trimmed = (raw ?? "").replace(/[\r\n]+/g, "").trim();
|
|
@@ -33,6 +33,12 @@ export interface LauncherConfig {
|
|
|
33
33
|
goalMode?: GoalModeDefault;
|
|
34
34
|
/** Context folding default (on when unset; `false` disables it for every launch). */
|
|
35
35
|
context?: boolean;
|
|
36
|
+
/** Default channel for `mnemom agent slack post` (the bot token lives in launcher.json). */
|
|
37
|
+
slackChannel?: string;
|
|
38
|
+
/** Attach the Slack channel on every launch (as if `--slack` were passed). */
|
|
39
|
+
slackListen?: boolean;
|
|
40
|
+
/** Slack member ids whose messages the router may deliver into a session. */
|
|
41
|
+
slackAllow?: string[];
|
|
36
42
|
/**
|
|
37
43
|
* Context-handoff default (EXPERIMENTAL, off when unset): `true` registers the `handoff`
|
|
38
44
|
* MCP tool and sends `CONTEXT_HANDOFF_ENABLED: "true"` in the per-session config header for
|
|
@@ -66,6 +66,17 @@ function coerceNonEmpty(name, raw) {
|
|
|
66
66
|
throw new Error(`${name} must be a non-empty string.`);
|
|
67
67
|
return v;
|
|
68
68
|
}
|
|
69
|
+
function coerceMemberIds(raw) {
|
|
70
|
+
const ids = raw
|
|
71
|
+
.split(/[\s,]+/)
|
|
72
|
+
.map((x) => x.trim())
|
|
73
|
+
.filter(Boolean);
|
|
74
|
+
const bad = ids.filter((x) => !/^[UW][A-Z0-9]{6,}$/.test(x));
|
|
75
|
+
if (!ids.length || bad.length) {
|
|
76
|
+
throw new Error(`slack_allow must be Slack member ids like U012ABCDEF (Profile → ⋯ → Copy member ID); got '${raw}'.`);
|
|
77
|
+
}
|
|
78
|
+
return ids;
|
|
79
|
+
}
|
|
69
80
|
function ensureGuardrails(cfg) {
|
|
70
81
|
if (!cfg.guardrails)
|
|
71
82
|
cfg.guardrails = {};
|
|
@@ -145,6 +156,30 @@ export const CONFIG_KEYS = [
|
|
|
145
156
|
c.context = coerceBool("context", v);
|
|
146
157
|
},
|
|
147
158
|
},
|
|
159
|
+
{
|
|
160
|
+
key: "slack_channel",
|
|
161
|
+
describe: "Default channel for `mnemom agent slack post` (e.g. #mnemom-allstars)",
|
|
162
|
+
get: (c) => c.slackChannel,
|
|
163
|
+
set: (c, v) => {
|
|
164
|
+
c.slackChannel = coerceNonEmpty("slack_channel", v);
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
key: "slack_listen",
|
|
169
|
+
describe: "Attach the Slack channel on every launch (as if --slack were passed)",
|
|
170
|
+
get: (c) => c.slackListen,
|
|
171
|
+
set: (c, v) => {
|
|
172
|
+
c.slackListen = coerceBool("slack_listen", v);
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
key: "slack_allow",
|
|
177
|
+
describe: "Slack member ids (comma-separated) whose messages may reach a session",
|
|
178
|
+
get: (c) => c.slackAllow?.join(","),
|
|
179
|
+
set: (c, v) => {
|
|
180
|
+
c.slackAllow = coerceMemberIds(v);
|
|
181
|
+
},
|
|
182
|
+
},
|
|
148
183
|
{
|
|
149
184
|
key: "context_handoff",
|
|
150
185
|
describe: "EXPERIMENTAL: register the `handoff` MCP tool + tell the gateway CONTEXT_HANDOFF_ENABLED=true (off by default)",
|
|
@@ -221,6 +256,14 @@ export function parseLauncherConfig(text) {
|
|
|
221
256
|
cfg.goalMode = coerceGoalMode(str(raw.goal_mode) ?? "");
|
|
222
257
|
if (raw.context !== undefined)
|
|
223
258
|
cfg.context = coerceBool("context", str(raw.context) ?? "");
|
|
259
|
+
if (raw.slack_listen !== undefined)
|
|
260
|
+
cfg.slackListen = coerceBool("slack_listen", str(raw.slack_listen) ?? "");
|
|
261
|
+
if (raw.slack_allow !== undefined) {
|
|
262
|
+
const v = raw.slack_allow;
|
|
263
|
+
cfg.slackAllow = coerceMemberIds(Array.isArray(v) ? v.map(String).join(",") : String(v));
|
|
264
|
+
}
|
|
265
|
+
if (raw.slack_channel !== undefined)
|
|
266
|
+
cfg.slackChannel = coerceNonEmpty("slack_channel", str(raw.slack_channel) ?? "");
|
|
224
267
|
if (raw.context_handoff !== undefined)
|
|
225
268
|
cfg.contextHandoff = coerceBool("context_handoff", str(raw.context_handoff) ?? "");
|
|
226
269
|
const g = raw.guardrails;
|
|
@@ -283,6 +326,12 @@ export function serializeLauncherConfig(cfg) {
|
|
|
283
326
|
doc.goal_mode = cfg.goalMode;
|
|
284
327
|
if (cfg.context !== undefined)
|
|
285
328
|
doc.context = cfg.context;
|
|
329
|
+
if (cfg.slackChannel !== undefined)
|
|
330
|
+
doc.slack_channel = cfg.slackChannel;
|
|
331
|
+
if (cfg.slackListen !== undefined)
|
|
332
|
+
doc.slack_listen = cfg.slackListen;
|
|
333
|
+
if (cfg.slackAllow !== undefined)
|
|
334
|
+
doc.slack_allow = cfg.slackAllow;
|
|
286
335
|
if (cfg.contextHandoff !== undefined)
|
|
287
336
|
doc.context_handoff = cfg.contextHandoff;
|
|
288
337
|
const g = cfg.guardrails;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/launcher-store.ts — the launcher's SECRET store, ~/.mnemom/launcher.json (0600).
|
|
3
|
+
*
|
|
4
|
+
* The human-editable settings live in launcher.toml (lib/launcher-config.ts); the
|
|
5
|
+
* credentials `mnemom agent` needs live here instead, so the file customers open,
|
|
6
|
+
* edit and share never carries one. Keys today:
|
|
7
|
+
*
|
|
8
|
+
* anthropic_key the provider key (commands/agent.ts)
|
|
9
|
+
* slack_bot_token the Slack bot token behind `mnemom agent slack` (lib/slack.ts)
|
|
10
|
+
*
|
|
11
|
+
* Every write MERGES into the existing document — a save of one secret must never
|
|
12
|
+
* erase another.
|
|
13
|
+
*/
|
|
14
|
+
export declare const LAUNCHER_STORE_PATH: string;
|
|
15
|
+
/** The store's JSON object, or {} when missing/corrupt. Never throws. */
|
|
16
|
+
export declare function readStore(path?: string): Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Merge `patch` into the store atomically (temp+rename, 0600). A `null` value
|
|
19
|
+
* removes that key. Throws on an fs failure so the caller can report it.
|
|
20
|
+
*/
|
|
21
|
+
export declare function mergeStore(patch: Record<string, unknown>, path?: string): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/launcher-store.ts — the launcher's SECRET store, ~/.mnemom/launcher.json (0600).
|
|
3
|
+
*
|
|
4
|
+
* The human-editable settings live in launcher.toml (lib/launcher-config.ts); the
|
|
5
|
+
* credentials `mnemom agent` needs live here instead, so the file customers open,
|
|
6
|
+
* edit and share never carries one. Keys today:
|
|
7
|
+
*
|
|
8
|
+
* anthropic_key the provider key (commands/agent.ts)
|
|
9
|
+
* slack_bot_token the Slack bot token behind `mnemom agent slack` (lib/slack.ts)
|
|
10
|
+
*
|
|
11
|
+
* Every write MERGES into the existing document — a save of one secret must never
|
|
12
|
+
* erase another.
|
|
13
|
+
*/
|
|
14
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { MNEMOM_DIR } from "./config.js";
|
|
17
|
+
export const LAUNCHER_STORE_PATH = join(MNEMOM_DIR, "launcher.json");
|
|
18
|
+
/** The store's JSON object, or {} when missing/corrupt. Never throws. */
|
|
19
|
+
export function readStore(path = LAUNCHER_STORE_PATH) {
|
|
20
|
+
try {
|
|
21
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
22
|
+
return data && typeof data === "object" && !Array.isArray(data)
|
|
23
|
+
? data
|
|
24
|
+
: {};
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return {};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Merge `patch` into the store atomically (temp+rename, 0600). A `null` value
|
|
32
|
+
* removes that key. Throws on an fs failure so the caller can report it.
|
|
33
|
+
*/
|
|
34
|
+
export function mergeStore(patch, path = LAUNCHER_STORE_PATH) {
|
|
35
|
+
const next = Object.fromEntries(Object.entries({ ...readStore(path), ...patch }).filter(([, v]) => v !== null));
|
|
36
|
+
const dir = dirname(path);
|
|
37
|
+
mkdirSync(dir, { recursive: true });
|
|
38
|
+
const tmp = join(dir, `.launcher-store-tmp-${process.pid}-${Date.now()}.json`);
|
|
39
|
+
writeFileSync(tmp, JSON.stringify(next, null, 2), { mode: 0o600 });
|
|
40
|
+
renameSync(tmp, path);
|
|
41
|
+
}
|
package/dist/lib/oauth.d.ts
CHANGED
|
@@ -133,14 +133,33 @@ export declare function refreshTokens(refreshToken: string, clientId: string, op
|
|
|
133
133
|
sleep?: (ms: number) => Promise<void>;
|
|
134
134
|
}): Promise<OAuthTokens | null>;
|
|
135
135
|
/**
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
136
|
+
* Parse and allowlist a URL before it is handed to an OS opener. Returns the
|
|
137
|
+
* normalized `href` (WHATWG-serialized, so spaces, quotes and other
|
|
138
|
+
* metacharacters are percent-encoded) or `null` when the URL must not be
|
|
139
|
+
* launched. Only `https:` is accepted, plus `http:` on a loopback host. This
|
|
140
|
+
* rejects `file:`, `javascript:`, custom protocol handlers and anything that
|
|
141
|
+
* could be read by an opener as a flag (a string starting with `-` is never a
|
|
142
|
+
* parseable absolute https URL).
|
|
142
143
|
*/
|
|
143
|
-
export declare function
|
|
144
|
+
export declare function validateBrowserUrl(url: string): string | null;
|
|
145
|
+
/**
|
|
146
|
+
* Resolve the platform opener as `[command, argv]`. No entry is a shell: the
|
|
147
|
+
* Windows path deliberately uses `rundll32 url.dll,FileProtocolHandler` rather
|
|
148
|
+
* than `cmd /c start`, because cmd.exe re-parses its command line and would
|
|
149
|
+
* interpret `&`, `|`, `^` etc. inside the URL (OAuth query strings always
|
|
150
|
+
* contain `&`) — both a truncation bug and a command-injection vector.
|
|
151
|
+
*/
|
|
152
|
+
export declare function browserOpenerFor(platform: NodeJS.Platform, href: string): [command: string, args: string[]];
|
|
153
|
+
/**
|
|
154
|
+
* Open `url` in the user's default browser WITHOUT a shell. The URL is first
|
|
155
|
+
* validated (https, or http on loopback) and then passed as a separate argv
|
|
156
|
+
* element to a non-shell opener, so shell metacharacters in it can never be
|
|
157
|
+
* interpreted as a command. Fire-and-forget: any failure (invalid URL, no
|
|
158
|
+
* opener installed, headless host) is swallowed because callers already print
|
|
159
|
+
* the URL as a manual fallback, so a missing browser must not crash login.
|
|
160
|
+
* Returns whether a launch was attempted.
|
|
161
|
+
*/
|
|
162
|
+
export declare function openBrowser(url: string): boolean;
|
|
144
163
|
/**
|
|
145
164
|
* Await `promise` while emitting a heartbeat tick every `intervalMs`, so a long
|
|
146
165
|
* silent wait (a browser sign-in round-trip) doesn't look hung. `onTick` is
|