waku-memory 0.3.0 → 0.4.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/.codex-plugin/plugin.json +2 -2
- package/dist/capture.js +9 -0
- package/dist/cli.js +60 -3
- package/dist/hook.js +1 -1
- package/dist/login.js +198 -39
- package/package.json +1 -1
- package/skills/waku/SKILL.md +10 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waku",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Waku Memory: durable memory for your Codex sessions. memory.recall brings what is known about a project into a session; memory.remember keeps what matters; the plugin's hooks open each session with a brief and send each turn to be remembered.",
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Waku Memory: durable memory for your Codex sessions. memory.recall brings what is known about a project into a session; memory.remember keeps what matters; the plugin's hooks open each session with a brief and send each turn to be remembered. What you type and what the agent replies is sent to Waku's servers and on to Anthropic, which is what turns a session into memories; your files, the commands the agent runs and its reasoning are not. This is an alpha and its data can be lost.",
|
|
5
5
|
"skills": "./skills/",
|
|
6
6
|
"hooks": "./hooks/hooks.json",
|
|
7
7
|
"interface": {
|
package/dist/capture.js
CHANGED
|
@@ -252,6 +252,15 @@ export function hookInvocation(execPath, indexPath) {
|
|
|
252
252
|
// Codex-only machine has no other way to learn that the same three hooks
|
|
253
253
|
// exist there too, or that installing the Waku plugin instead is the other
|
|
254
254
|
// route to the same events (codexPluginPresent, this file's own enable()).
|
|
255
|
+
//
|
|
256
|
+
// This text now exists in four places with no mechanism sharing it (found
|
|
257
|
+
// 2026-09-10, status.md item 9's dated paragraph): this DISCLOSURE, the
|
|
258
|
+
// Codex plugin manifest's "description" (.codex-plugin/plugin.json, pinned
|
|
259
|
+
// by plugin.test.mjs), skills/waku/SKILL.md's "sent to Waku" bullet, and
|
|
260
|
+
// the frontend's OAuth consent page (waku-memory-frontend
|
|
261
|
+
// app/oauth/consent/page.tsx, its CONSENT_DISCLOSURE constant). A change to
|
|
262
|
+
// the two facts here -- where captured content goes, and that this is an
|
|
263
|
+
// alpha -- has to be carried to the other three by hand.
|
|
255
264
|
export const DISCLOSURE = 'Captured content is sent to our servers and to Anthropic for extraction. ' +
|
|
256
265
|
'This is an alpha whose data can be lost. ' +
|
|
257
266
|
'What you type, what the agent replies, and the names of the tools it uses are sent; ' +
|
package/dist/cli.js
CHANGED
|
@@ -133,6 +133,10 @@ export function printUsage() {
|
|
|
133
133
|
console.log(` --name key to add under mcpServers (default: "${DEFAULT_NAME}")`);
|
|
134
134
|
console.log(` --url server URL to write (default: ${DEFAULT_URL})`);
|
|
135
135
|
console.log('');
|
|
136
|
+
console.log(' If a harness was found, setup then asks whether to also turn on automatic');
|
|
137
|
+
console.log(' capture -- the same flow as "capture enable", asked so you do not have to');
|
|
138
|
+
console.log(' type a second command. Default is no; press Enter to skip it.');
|
|
139
|
+
console.log('');
|
|
136
140
|
console.log('Usage: waku-memory hook [--harness <claude_code|codex>]');
|
|
137
141
|
console.log('');
|
|
138
142
|
console.log(' Reads one hook event as JSON on stdin and reports it to the server named');
|
|
@@ -464,13 +468,49 @@ export function summarize(results) {
|
|
|
464
468
|
hadError: results.some((r) => r.status === 'error'),
|
|
465
469
|
};
|
|
466
470
|
}
|
|
467
|
-
|
|
471
|
+
// The one-shot readline prompt setup()'s own new question needs, real
|
|
472
|
+
// default for SetupPrompt below -- a plain rl.question(), not the
|
|
473
|
+
// async-iterator dance runCaptureCommand's own prompt uses for enable()'s
|
|
474
|
+
// two sequential questions (see that function's own comment): setup() only
|
|
475
|
+
// ever asks one question of its own before handing off to runCaptureEnable,
|
|
476
|
+
// which opens (and closes) its own readline in turn, so there is no shared
|
|
477
|
+
// interface for two overlapping .question() calls to race over.
|
|
478
|
+
function realSetupPrompt(question) {
|
|
479
|
+
return new Promise((resolve) => {
|
|
480
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
481
|
+
rl.question(question, (answer) => {
|
|
482
|
+
rl.close();
|
|
483
|
+
resolve(answer);
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
// Verbatim -- cli.test.mjs depends on this exact string, the same contract
|
|
488
|
+
// capture.ts's own SIGN_IN_QUESTION carries.
|
|
489
|
+
export const TURN_ON_CAPTURE_QUESTION = 'Also turn on automatic capture? It sends what you type and what the agent replies to Waku. [y/N]: ';
|
|
490
|
+
export async function setup(name, url, harnesses = getHarnesses(),
|
|
468
491
|
// R20 (final review, spec 012): the real default, injected the same way
|
|
469
492
|
// realCaptureDeps below builds it -- so cli.test.mjs can point this at a
|
|
470
493
|
// temp path instead of the machine's actual ~/.codex/config.toml, the
|
|
471
494
|
// same reason every other real path in this file is a parameter with a
|
|
472
495
|
// real default rather than a bare homedir() call inline.
|
|
473
|
-
codexConfigPath = join(homedir(), '.codex', 'config.toml')
|
|
496
|
+
codexConfigPath = join(homedir(), '.codex', 'config.toml'),
|
|
497
|
+
// The two additions that make setup() async (this task): the question
|
|
498
|
+
// itself (realSetupPrompt, above) and what a "y" answer actually runs.
|
|
499
|
+
// Both injectable, the same reason every real path/writer/fetch in this
|
|
500
|
+
// file is a parameter with a real default rather than a bare call inline
|
|
501
|
+
// -- so cli.test.mjs never opens real stdin and never runs a real
|
|
502
|
+
// capture enable. The real runCaptureEnable below is nothing but the
|
|
503
|
+
// existing "capture enable" command path (runCaptureCommand, same as
|
|
504
|
+
// dispatch()'s own 'capture'/'enable' case), called with no flags -- the
|
|
505
|
+
// Sign-in question, enable()'s own disclosure and typed "y" gate, the
|
|
506
|
+
// credential step, the hooks, and the bootstrap question all run exactly
|
|
507
|
+
// as they do from the command line. Nothing here may skip, weaken, or
|
|
508
|
+
// pre-answer any of that: the only thing this buys the person is not
|
|
509
|
+
// having to type "npx waku-memory capture enable" a second time.
|
|
510
|
+
prompt = realSetupPrompt, runCaptureEnable = () => {
|
|
511
|
+
const { ingestUrl, mcpUrl } = urlPair(url);
|
|
512
|
+
return runCaptureCommand('enable', ingestUrl, mcpUrl, DEFAULT_BOOTSTRAP, { kind: 'ask' });
|
|
513
|
+
}) {
|
|
474
514
|
const results = runSetup(harnesses, name, url);
|
|
475
515
|
for (const r of results) {
|
|
476
516
|
if (r.status === 'error')
|
|
@@ -523,6 +563,20 @@ codexConfigPath = join(homedir(), '.codex', 'config.toml')) {
|
|
|
523
563
|
}
|
|
524
564
|
if (hadError)
|
|
525
565
|
process.exitCode = 1;
|
|
566
|
+
// The new question this task adds: asked only once there is something to
|
|
567
|
+
// turn capture on for -- a machine with no harness found has nothing for
|
|
568
|
+
// "capture enable" to install into either, so asking would just repeat
|
|
569
|
+
// the "nothing was added" message above in question form. Default is no
|
|
570
|
+
// (an empty answer, from pressing Enter, keeps today's behaviour exactly)
|
|
571
|
+
// -- capture is a much bigger step than writing an MCP config, so unlike
|
|
572
|
+
// enable()'s own SIGN_IN_QUESTION (whose default path still requires
|
|
573
|
+
// choosing 1 or 2), this one must never be opted into by accident.
|
|
574
|
+
if (foundAny) {
|
|
575
|
+
const answer = (await prompt(TURN_ON_CAPTURE_QUESTION)).trim().toLowerCase();
|
|
576
|
+
if (answer === 'y') {
|
|
577
|
+
await runCaptureEnable();
|
|
578
|
+
}
|
|
579
|
+
}
|
|
526
580
|
}
|
|
527
581
|
function errorMessage(err) {
|
|
528
582
|
return err instanceof Error ? err.message : String(err);
|
|
@@ -822,7 +876,10 @@ export function run(argv) {
|
|
|
822
876
|
process.exitCode = 1;
|
|
823
877
|
return;
|
|
824
878
|
case 'setup':
|
|
825
|
-
setup(d.name, d.url)
|
|
879
|
+
void setup(d.name, d.url).catch((err) => {
|
|
880
|
+
console.error(`waku-memory setup: unexpected failure -- ${errorMessage(err)}.`);
|
|
881
|
+
process.exitCode = 1;
|
|
882
|
+
});
|
|
826
883
|
return;
|
|
827
884
|
case 'login':
|
|
828
885
|
// Ruling R15: the disclosure (and a blank line), no y/n gate -- unlike
|
package/dist/hook.js
CHANGED
|
@@ -63,7 +63,7 @@ export { SESSION_END_HOOK_TIMEOUT_S, SESSION_END_TIMEOUT_MS } from "./harnesses.
|
|
|
63
63
|
// hook.test.mjs's drift check (against package.json) both need it; task 14
|
|
64
64
|
// (spec 012 §9) bumps this, package.json and the plugin manifest
|
|
65
65
|
// (.codex-plugin/plugin.json) to 0.3.0 together.
|
|
66
|
-
export const SHIM_VERSION = '0.
|
|
66
|
+
export const SHIM_VERSION = '0.4.1';
|
|
67
67
|
// Exported for capture.ts, which writes the file this module reads.
|
|
68
68
|
export const CONFIG_FILE_NAME = 'config.json';
|
|
69
69
|
// Same-directory temp file + rename, exactly atomicWriteJson's technique in
|
package/dist/login.js
CHANGED
|
@@ -33,7 +33,122 @@ export const LOGIN_SCOPES = 'openid profile email offline_access';
|
|
|
33
33
|
// default apiBase, so a reader (or a test asserting against the default)
|
|
34
34
|
// has something to compare the runtime value to without recomputing it.
|
|
35
35
|
export const LOGIN_RESOURCE = 'https://api.waku.one/mcp';
|
|
36
|
-
const
|
|
36
|
+
const CALLBACK_PAGE_COPY = {
|
|
37
|
+
// The real success. Only this one gets the ok/accent treatment -- the
|
|
38
|
+
// system's own message vocabulary (§14 of the design doc) marks a
|
|
39
|
+
// level with an uppercase mono word before the sentence, not with a
|
|
40
|
+
// filled surface or an icon.
|
|
41
|
+
'signed-in': {
|
|
42
|
+
title: 'Signed in',
|
|
43
|
+
mark: 'OK',
|
|
44
|
+
markColor: 'var(--ok)',
|
|
45
|
+
heading: 'Signed in.',
|
|
46
|
+
body: 'This tab can be closed now.',
|
|
47
|
+
},
|
|
48
|
+
// The duplicate callback (`accepted` already true): the flow already
|
|
49
|
+
// finished, so this is not an error and carries no color.
|
|
50
|
+
'already-signed-in': {
|
|
51
|
+
title: 'Already signed in',
|
|
52
|
+
mark: '·',
|
|
53
|
+
markColor: 'var(--text-faint)',
|
|
54
|
+
heading: 'Already signed in.',
|
|
55
|
+
body: 'Nothing else to do here.',
|
|
56
|
+
},
|
|
57
|
+
// The `state` mismatch or missing `code` (400). Neither an ok nor a
|
|
58
|
+
// plain neutral: a mismatched state can mean the link was tampered
|
|
59
|
+
// with, so this reads as the doc's `warn` level -- worth noticing,
|
|
60
|
+
// not a failure to panic over -- and says the one thing that is a
|
|
61
|
+
// different action from reloading this tab: go back to the terminal.
|
|
62
|
+
'could-not-complete': {
|
|
63
|
+
title: 'Sign-in could not complete',
|
|
64
|
+
mark: 'WARN',
|
|
65
|
+
markColor: 'var(--warn)',
|
|
66
|
+
heading: 'Sign-in did not complete.',
|
|
67
|
+
body: 'The command is still running. Try again from the terminal.',
|
|
68
|
+
},
|
|
69
|
+
// Any path but /callback (404). Nothing to explain beyond the one fact.
|
|
70
|
+
'not-found': {
|
|
71
|
+
title: 'Not found',
|
|
72
|
+
mark: '·',
|
|
73
|
+
markColor: 'var(--text-faint)',
|
|
74
|
+
heading: 'Not found.',
|
|
75
|
+
body: 'This address is not part of sign-in.',
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
function renderCallbackPage(state) {
|
|
79
|
+
const copy = CALLBACK_PAGE_COPY[state];
|
|
80
|
+
return `<!doctype html>
|
|
81
|
+
<html lang="en">
|
|
82
|
+
<head>
|
|
83
|
+
<meta charset="utf-8">
|
|
84
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
85
|
+
<title>${copy.title}</title>
|
|
86
|
+
<style>
|
|
87
|
+
:root {
|
|
88
|
+
--ground: #E1E1D9;
|
|
89
|
+
--ink: #161614;
|
|
90
|
+
--accent-ink: #161614;
|
|
91
|
+
--ok: #2B7754;
|
|
92
|
+
--warn: #8C5617;
|
|
93
|
+
--text-muted: color-mix(in srgb, var(--ink) 82%, var(--ground));
|
|
94
|
+
--text-faint: color-mix(in srgb, var(--ink) 62%, var(--ground));
|
|
95
|
+
}
|
|
96
|
+
@media (prefers-color-scheme: dark) {
|
|
97
|
+
:root {
|
|
98
|
+
--ground: #202020;
|
|
99
|
+
--ink: #C9CDD1;
|
|
100
|
+
--ok: #4FBF8B;
|
|
101
|
+
--warn: #E78B23;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
* { box-sizing: border-box; }
|
|
105
|
+
html, body { height: 100%; margin: 0; }
|
|
106
|
+
body {
|
|
107
|
+
display: flex;
|
|
108
|
+
align-items: center;
|
|
109
|
+
justify-content: center;
|
|
110
|
+
padding: 24px;
|
|
111
|
+
background: var(--ground);
|
|
112
|
+
color: var(--ink);
|
|
113
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
|
|
114
|
+
font-weight: 400;
|
|
115
|
+
text-align: center;
|
|
116
|
+
}
|
|
117
|
+
main { max-width: 40ch; }
|
|
118
|
+
.mark {
|
|
119
|
+
display: block;
|
|
120
|
+
margin: 0 0 12px;
|
|
121
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
122
|
+
font-weight: 500;
|
|
123
|
+
font-size: 12px;
|
|
124
|
+
letter-spacing: .16em;
|
|
125
|
+
text-transform: uppercase;
|
|
126
|
+
color: ${copy.markColor};
|
|
127
|
+
}
|
|
128
|
+
h1 {
|
|
129
|
+
margin: 0 0 12px;
|
|
130
|
+
font-size: 20px;
|
|
131
|
+
font-weight: 500;
|
|
132
|
+
line-height: 1.3;
|
|
133
|
+
}
|
|
134
|
+
p {
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: 15px;
|
|
137
|
+
line-height: 1.5;
|
|
138
|
+
color: var(--text-muted);
|
|
139
|
+
}
|
|
140
|
+
</style>
|
|
141
|
+
</head>
|
|
142
|
+
<body>
|
|
143
|
+
<main>
|
|
144
|
+
<span class="mark">${copy.mark}</span>
|
|
145
|
+
<h1>${copy.heading}</h1>
|
|
146
|
+
<p>${copy.body}</p>
|
|
147
|
+
</main>
|
|
148
|
+
</body>
|
|
149
|
+
</html>
|
|
150
|
+
`;
|
|
151
|
+
}
|
|
37
152
|
// Carries which of the four network steps failed and the HTTP status that
|
|
38
153
|
// said so -- login()'s refusal branches read both off this and print
|
|
39
154
|
// neither a response body nor anything from the request (never the token,
|
|
@@ -83,18 +198,25 @@ export async function discover(apiBase, fetchImpl) {
|
|
|
83
198
|
registrationEndpoint: registration_endpoint,
|
|
84
199
|
};
|
|
85
200
|
}
|
|
86
|
-
// Dynamic client registration (RFC 7591): a public client,
|
|
87
|
-
//
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
|
|
201
|
+
// Dynamic client registration (RFC 7591): a public client, registered with
|
|
202
|
+
// the exact redirect URI login() will use for the authorize request and the
|
|
203
|
+
// token exchange -- including the ephemeral port the loopback listener is
|
|
204
|
+
// actually on. RFC 8252 §7.3 lets a client register the bare loopback
|
|
205
|
+
// address and redirect to any port at request time, but our authorization
|
|
206
|
+
// server (Supabase) exact-matches redirect_uri against what was registered
|
|
207
|
+
// and does not implement that exception (measured against the deployed
|
|
208
|
+
// service, 2026-09-08: registering the bare address and then authorizing
|
|
209
|
+
// against the real port got back `{"error_code":"validation_failed","msg":
|
|
210
|
+
// "invalid redirect_uri"}`). So the caller must start the listener first and
|
|
211
|
+
// pass its real redirect URI in here -- see login()'s own ordering. Returns
|
|
212
|
+
// client_id; throws StepError('registration', status) otherwise.
|
|
213
|
+
export async function registerClient(registrationEndpoint, fetchImpl, version, redirectUri) {
|
|
92
214
|
const res = await fetchImpl(registrationEndpoint, {
|
|
93
215
|
method: 'POST',
|
|
94
216
|
headers: { 'Content-Type': 'application/json' },
|
|
95
217
|
body: JSON.stringify({
|
|
96
218
|
client_name: `waku-memory ${version}`,
|
|
97
|
-
redirect_uris: [
|
|
219
|
+
redirect_uris: [redirectUri],
|
|
98
220
|
grant_types: ['authorization_code'],
|
|
99
221
|
response_types: ['code'],
|
|
100
222
|
token_endpoint_auth_method: 'none',
|
|
@@ -187,10 +309,20 @@ function reportRefusal(deps, fallbackStep, err) {
|
|
|
187
309
|
// only things written to deps.stdout are the authorize URL, the final
|
|
188
310
|
// "Signed in as <email>" line, and (on refusal) the one-line step/status
|
|
189
311
|
// report above. The loopback server, once opened, is closed on every path
|
|
190
|
-
// out of the returned promise: 'signed-in', 'timeout', and 'refused' from
|
|
191
|
-
// a bad
|
|
192
|
-
// 'refused' from discovery
|
|
193
|
-
// exists, so there is nothing to close on
|
|
312
|
+
// out of the returned promise: 'signed-in', 'timeout', and 'refused' from a
|
|
313
|
+
// listener failure, a bad registration, or a bad token/keys response all go
|
|
314
|
+
// through the same finish(). 'refused' from discovery returns before any
|
|
315
|
+
// server exists, so there is nothing to close on that one path.
|
|
316
|
+
//
|
|
317
|
+
// Ordering (fixed 2026-09-08, measured against the deployed service): the
|
|
318
|
+
// listener starts FIRST, before registration, so registerClient can be
|
|
319
|
+
// given the real `http://127.0.0.1:<port>/callback` -- the same string then
|
|
320
|
+
// used, unchanged, for the authorize URL and the token exchange. Registering
|
|
321
|
+
// the bare loopback address ahead of knowing the port (the previous order)
|
|
322
|
+
// is what RFC 8252 §7.3 expects a server to accept via its any-port
|
|
323
|
+
// exception, but Supabase exact-matches redirect_uri against the registered
|
|
324
|
+
// list and answered with `invalid redirect_uri`: see registerClient's own
|
|
325
|
+
// comment for the exact response.
|
|
194
326
|
export async function login(deps) {
|
|
195
327
|
const apiBase = stripSlash(deps.apiBase);
|
|
196
328
|
const resource = `${apiBase}/mcp`;
|
|
@@ -203,25 +335,18 @@ export async function login(deps) {
|
|
|
203
335
|
reportRefusal(deps, 'discovery', err);
|
|
204
336
|
return 'refused';
|
|
205
337
|
}
|
|
206
|
-
let clientId;
|
|
207
|
-
try {
|
|
208
|
-
clientId = await registerClient(discovered.registrationEndpoint, deps.fetchImpl, deps.version);
|
|
209
|
-
}
|
|
210
|
-
catch (err) {
|
|
211
|
-
reportRefusal(deps, 'registration', err);
|
|
212
|
-
return 'refused';
|
|
213
|
-
}
|
|
214
338
|
const { verifier, challenge } = pkce();
|
|
215
339
|
const state = randomBytes(16).toString('hex');
|
|
216
340
|
return new Promise((resolveLogin) => {
|
|
217
341
|
let settled = false;
|
|
218
|
-
let port = 0;
|
|
219
342
|
let accepted = false;
|
|
343
|
+
let clientId = '';
|
|
344
|
+
let redirectUri = '';
|
|
220
345
|
const server = createServer((req, res) => {
|
|
221
346
|
const requestUrl = new URL(req.url ?? '/', 'http://127.0.0.1');
|
|
222
347
|
if (requestUrl.pathname !== '/callback') {
|
|
223
|
-
res.writeHead(404);
|
|
224
|
-
res.end();
|
|
348
|
+
res.writeHead(404, { 'Content-Type': 'text/html' });
|
|
349
|
+
res.end(renderCallbackPage('not-found'));
|
|
225
350
|
return;
|
|
226
351
|
}
|
|
227
352
|
const code = requestUrl.searchParams.get('code');
|
|
@@ -230,8 +355,8 @@ export async function login(deps) {
|
|
|
230
355
|
// Wrong or missing state: keep waiting -- a stray or forged hit on
|
|
231
356
|
// this port must not end the flow the real browser is still
|
|
232
357
|
// carrying.
|
|
233
|
-
res.writeHead(400);
|
|
234
|
-
res.end();
|
|
358
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
359
|
+
res.end(renderCallbackPage('could-not-complete'));
|
|
235
360
|
return;
|
|
236
361
|
}
|
|
237
362
|
// OAuth codes are single-use and minting a credential should be
|
|
@@ -241,12 +366,12 @@ export async function login(deps) {
|
|
|
241
366
|
// sees the flag and returns 200 without re-running the exchange.
|
|
242
367
|
if (accepted) {
|
|
243
368
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
244
|
-
res.end('
|
|
369
|
+
res.end(renderCallbackPage('already-signed-in'));
|
|
245
370
|
return;
|
|
246
371
|
}
|
|
247
372
|
accepted = true;
|
|
248
373
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
249
|
-
res.end('
|
|
374
|
+
res.end(renderCallbackPage('signed-in'));
|
|
250
375
|
void finishSignIn(code);
|
|
251
376
|
});
|
|
252
377
|
const finish = (result) => {
|
|
@@ -254,12 +379,25 @@ export async function login(deps) {
|
|
|
254
379
|
return;
|
|
255
380
|
settled = true;
|
|
256
381
|
clearTimeout(timer);
|
|
382
|
+
// server.close() alone only stops accepting *new* connections; it
|
|
383
|
+
// waits for existing ones to end on their own before the server (and
|
|
384
|
+
// its socket handles) actually goes away. A real browser holds the
|
|
385
|
+
// callback connection open with HTTP keep-alive -- it never ends the
|
|
386
|
+
// connection itself -- so that lone open socket kept the event loop
|
|
387
|
+
// alive and the process never exited (measured 2026-09-08 against
|
|
388
|
+
// the deployed service: a real sign-in completed, config.json was
|
|
389
|
+
// written, and the process was still running 92s later). Dropping
|
|
390
|
+
// every outstanding connection here, active or idle, is what lets
|
|
391
|
+
// the server -- and the process -- actually finish closing, on every
|
|
392
|
+
// path through finish(): signed-in, timeout, and refused alike.
|
|
393
|
+
// closeAllConnections() has been available since Node 18.2; this
|
|
394
|
+
// package requires Node >=20 (see package.json's engines).
|
|
257
395
|
server.close();
|
|
396
|
+
server.closeAllConnections();
|
|
258
397
|
resolveLogin(result);
|
|
259
398
|
};
|
|
260
399
|
async function finishSignIn(code) {
|
|
261
400
|
try {
|
|
262
|
-
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
263
401
|
const tokenResult = await exchangeToken(discovered.tokenEndpoint, deps.fetchImpl, {
|
|
264
402
|
code,
|
|
265
403
|
redirectUri,
|
|
@@ -293,20 +431,41 @@ export async function login(deps) {
|
|
|
293
431
|
}
|
|
294
432
|
}
|
|
295
433
|
const timer = setTimeout(() => finish('timeout'), timeoutMs);
|
|
434
|
+
// A listener that never comes up (EADDRINUSE, permissions, ...) fires
|
|
435
|
+
// 'error' instead of the 'listening' callback below -- reported the same
|
|
436
|
+
// way every other step is, naming 'listener' rather than falling through
|
|
437
|
+
// to a confusing StepError from somewhere else.
|
|
438
|
+
server.on('error', (err) => {
|
|
439
|
+
reportRefusal(deps, 'listener', err);
|
|
440
|
+
finish('refused');
|
|
441
|
+
});
|
|
296
442
|
server.listen(0, '127.0.0.1', () => {
|
|
297
443
|
const address = server.address();
|
|
298
|
-
port = typeof address === 'object' && address !== null ? address.port : 0;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
444
|
+
const port = typeof address === 'object' && address !== null ? address.port : 0;
|
|
445
|
+
if (port === 0) {
|
|
446
|
+
reportRefusal(deps, 'listener', new Error('the loopback listener did not report a port'));
|
|
447
|
+
finish('refused');
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
451
|
+
registerClient(discovered.registrationEndpoint, deps.fetchImpl, deps.version, redirectUri)
|
|
452
|
+
.then((id) => {
|
|
453
|
+
clientId = id;
|
|
454
|
+
const url = authorizeUrl({
|
|
455
|
+
authorizationEndpoint: discovered.authorizationEndpoint,
|
|
456
|
+
clientId,
|
|
457
|
+
redirectUri,
|
|
458
|
+
state,
|
|
459
|
+
challenge,
|
|
460
|
+
resource,
|
|
461
|
+
});
|
|
462
|
+
deps.stdout(`Open this URL if your browser did not: ${url}`);
|
|
463
|
+
deps.openBrowser(url);
|
|
464
|
+
})
|
|
465
|
+
.catch((err) => {
|
|
466
|
+
reportRefusal(deps, 'registration', err);
|
|
467
|
+
finish('refused');
|
|
307
468
|
});
|
|
308
|
-
deps.stdout(`Open this URL if your browser did not: ${url}`);
|
|
309
|
-
deps.openBrowser(url);
|
|
310
469
|
});
|
|
311
470
|
});
|
|
312
471
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "waku-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Install Waku Memory into your agent harness, and turn on automatic session capture.",
|
|
5
5
|
"keywords": ["mcp", "memory", "claude-code", "agent", "waku"],
|
|
6
6
|
"homepage": "https://github.com/ShenSeanChen/waku-memory-backend/tree/spec-driven/shim#readme",
|
package/skills/waku/SKILL.md
CHANGED
|
@@ -16,8 +16,17 @@ learn, and brings it back into later sessions.
|
|
|
16
16
|
a procedure or a fact they will want next time. Use their words. Do not
|
|
17
17
|
store secrets, credentials or file contents.
|
|
18
18
|
- What the person types and what you reply is sent to Waku after each turn
|
|
19
|
-
by the plugin's hooks; tool output is not.
|
|
19
|
+
by the plugin's hooks; tool output is not. It goes on to Anthropic, which
|
|
20
|
+
is what turns a session into memories. This is an alpha and its data can
|
|
21
|
+
be lost.
|
|
20
22
|
|
|
21
23
|
One-time setup the person does: after installing, Codex asks them to
|
|
22
24
|
review and trust this plugin's hooks (`/hooks` in the CLI). Until then
|
|
23
25
|
the brief and the capture do not run.
|
|
26
|
+
|
|
27
|
+
<!-- This bullet's two facts (where captured content goes, and that this
|
|
28
|
+
is an alpha) also live in capture.ts's DISCLOSURE, the Codex plugin
|
|
29
|
+
manifest's "description" (.codex-plugin/plugin.json), and the
|
|
30
|
+
frontend's OAuth consent page (waku-memory-frontend
|
|
31
|
+
app/oauth/consent/page.tsx's CONSENT_DISCLOSURE). No mechanism shares
|
|
32
|
+
the string; a change here has to be carried to the other three. -->
|