@stage-labs/metro 0.1.0-beta.235 → 0.1.0-beta.236
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/cli.js +7 -5
- package/dist/memory.js +2 -2
- package/package.json +1 -1
- package/runtime/marketplace/plugin/.claude-plugin/plugin.json +1 -1
- package/runtime/marketplace/plugin/MEMORY.md +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/agent-user/schedule-detail.ts +2 -2
- package/runtime/node_modules/@metro-labs/daemon/src/claude/memory-routine.ts +25 -16
- package/runtime/node_modules/@metro-labs/daemon/src/claude/setup.ts +1 -1
- package/runtime/runtime.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@ import { whoisAuthorized } from './api.js';
|
|
|
3
3
|
import { stopAll } from './control.js';
|
|
4
4
|
import { tailEvents } from './tail.js';
|
|
5
5
|
import { launchClaude } from './claude.js';
|
|
6
|
-
import { memoryRoutine } from './memory.js';
|
|
6
|
+
import { memoryPaths, memoryRoutine } from './memory.js';
|
|
7
7
|
import { update } from './update.js';
|
|
8
8
|
import { serve } from './serve.js';
|
|
9
9
|
import { service, serviceStopHint } from './service.js';
|
|
@@ -32,9 +32,11 @@ const USAGE = `metro: run your agent on this machine
|
|
|
32
32
|
open Claude Code with the metro channel and the metro MCP server, its
|
|
33
33
|
inference routed through the daemon's model gateway (the Model page);
|
|
34
34
|
every argument is passed through the same
|
|
35
|
-
metro memory
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
metro memory [skill file]
|
|
36
|
+
run the memory routine once: when Claude Code transcripts changed since
|
|
37
|
+
the last run, Claude Code reads the memory skill (or the file given) and
|
|
38
|
+
files them into its memory, with no metro tools and writes only inside
|
|
39
|
+
the memory folder
|
|
38
40
|
metro update update to the newest published version (--check only reports)
|
|
39
41
|
metro version print this CLI's version
|
|
40
42
|
|
|
@@ -68,7 +70,7 @@ const COMMANDS = {
|
|
|
68
70
|
stop: stopDaemon,
|
|
69
71
|
tail: () => tailEvents(process.argv.slice(3)),
|
|
70
72
|
claude: () => launchClaude(process.argv.slice(3)),
|
|
71
|
-
memory: () => memoryRoutine(),
|
|
73
|
+
memory: () => memoryRoutine(memoryPaths(process.env, process.argv[3])),
|
|
72
74
|
update: () => update(process.argv.slice(3)),
|
|
73
75
|
version: async () => {
|
|
74
76
|
process.stdout.write(`${currentVersion()}\n`);
|
package/dist/memory.js
CHANGED
|
@@ -46,13 +46,13 @@ function customDir(claude, home) {
|
|
|
46
46
|
return undefined;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
export function memoryPaths(env = process.env) {
|
|
49
|
+
export function memoryPaths(env = process.env, skill) {
|
|
50
50
|
const home = given(env.HOME) ?? homedir();
|
|
51
51
|
const claude = claudeDir(env);
|
|
52
52
|
return {
|
|
53
53
|
claude,
|
|
54
54
|
memory: customDir(claude, home) ?? join(projectDir(claude, realHome(home)), 'memory'),
|
|
55
|
-
skill: join(claude, 'skills', 'memory', 'SKILL.md'),
|
|
55
|
+
skill: given(skill) ?? join(claude, 'skills', 'memory', 'SKILL.md'),
|
|
56
56
|
state: join(home, '.metro', 'memory-routine.json'),
|
|
57
57
|
lock: join(home, '.metro', 'memory-routine.lock'),
|
|
58
58
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "metro",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.236",
|
|
4
4
|
"description": "Metro for Claude Code, installed by the metro daemon on this machine: every connector the agent holds as its own MCP server relayed through the daemon, the orchestrator-only guard on the main thread, and the standing rules loaded at each session start. No vendor credential ever sits in a config file.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Bonustrack",
|
|
@@ -3,7 +3,7 @@ name: memory
|
|
|
3
3
|
description: How this agent keeps its long-term memory in the Instinct layout (people, organizations, facts, preferences, decisions, conversation digests, daily and weekly timelines, active and completed work), the note format and rules, and the daily routine that files each day into it. Use when saving or looking something up in memory, and when the daily memory routine runs.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
Your memory is a folder of Markdown notes, found with grep and links, not a database. The layout follows Instinct (https://supermemory.ai/blog/reverse-engineering-instinct-memory/).
|
|
6
|
+
Your memory is a folder of Markdown notes, found with grep and links, not a database. The layout follows Instinct (https://supermemory.ai/blog/reverse-engineering-instinct-memory/). Twice a day, at 00:00 and 12:00 UTC, a scheduled run (the `memory-routine` job on the Scheduled page) reads this skill and files what happened into it. You can use the same rules any time you save something.
|
|
7
7
|
|
|
8
8
|
**Where it is.** The auto-memory directory of your Claude Code home folder: `~/.claude/projects/<home folder, with every character that is not a letter or digit turned into ->/memory/` (for `/home/agent` that is `~/.claude/projects/-home-agent/memory/`), or the `autoMemoryDirectory` set in `~/.claude/settings.json`. The daily run names it in its prompt.
|
|
9
9
|
|
|
@@ -72,7 +72,7 @@ The scheduled run has no chat tools, no shell, no network and no subagents. It c
|
|
|
72
72
|
- Work goes into `workstreams/active/`. When it finished, only set its `status` in place (see Rules).
|
|
73
73
|
- Each chat line gets a digest in `comms/phone/`.
|
|
74
74
|
- Turn passing anecdotes into general traits. Drop trivia.
|
|
75
|
-
4. Write `timeline/daily/YYYY-MM-DD.md` for each UTC day with activity since the last run: what happened, with links. Things that need the owner go as one line each under a `## Needs <owner's name>` heading, since this run cannot message anyone.
|
|
75
|
+
4. Write `timeline/daily/YYYY-MM-DD.md` for each UTC day with activity since the last run: what happened, with links. A note an earlier run already wrote gets added to. Things that need the owner go as one line each under a `## Needs <owner's name>` heading, since this run cannot message anyone.
|
|
76
76
|
5. If the prompt names a previous ISO week whose `timeline/weekly/YYYY-Www.md` does not exist yet, and there are daily notes for that week, write it from those daily notes.
|
|
77
77
|
6. Tidy up. Merge duplicates into one note, and keep the other slug in its `aliases`. Cut the other note down to its frontmatter and one line: `Merged into [[slug]].` Fix broken `[[links]]` and update `MEMORY.md`.
|
|
78
78
|
7. End with at most three lines on what changed.
|
|
@@ -29,8 +29,8 @@ function textFile(path: string): string | null {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function scriptOf(command: string): { path: string; text: string } | null {
|
|
32
|
-
const words = command.split(/\s+/).filter((w) => w.startsWith('/') && !w.includes('>'));
|
|
33
|
-
for (const path of words.
|
|
32
|
+
const words = command.split(/\s+/).filter((w) => w.startsWith('/') && !w.includes('>')).slice(0, 3);
|
|
33
|
+
for (const path of [...words.filter((w) => w.endsWith('.md')), ...words]) {
|
|
34
34
|
const text = textFile(path);
|
|
35
35
|
if (text !== null) return { path, text };
|
|
36
36
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
|
-
import { hostname } from 'node:os';
|
|
3
1
|
import { join } from 'node:path';
|
|
4
2
|
import { errMsg, log } from '@metro-labs/core/log';
|
|
5
3
|
import { existsSync, readFileSync } from '../agent-user/agent-fs.js';
|
|
@@ -10,8 +8,9 @@ import { agentPath, agentUser, type AgentUser } from '../agent-user/user.js';
|
|
|
10
8
|
|
|
11
9
|
const BEGIN = '# metro memory routine: begin';
|
|
12
10
|
const END = '# metro memory routine: end';
|
|
11
|
+
const REPLACED = '# replaced by memory-routine: ';
|
|
13
12
|
const OWN_JOB = /memory/i;
|
|
14
|
-
const
|
|
13
|
+
const SCHEDULE = '0 0,12 * * *';
|
|
15
14
|
|
|
16
15
|
export type MemoryJobState = 'scheduled' | 'own' | 'off' | 'unavailable';
|
|
17
16
|
|
|
@@ -25,7 +24,6 @@ export interface MemoryJobDeps {
|
|
|
25
24
|
runner?: Runner;
|
|
26
25
|
cli?: string;
|
|
27
26
|
port?: string;
|
|
28
|
-
seed?: string;
|
|
29
27
|
}
|
|
30
28
|
|
|
31
29
|
let last: MemoryJob = { state: 'unavailable', job: null };
|
|
@@ -33,6 +31,7 @@ let last: MemoryJob = { state: 'unavailable', job: null };
|
|
|
33
31
|
export const memoryJobStatus = (): MemoryJob => ({ ...last });
|
|
34
32
|
|
|
35
33
|
export const launcherPath = (user: AgentUser): string => join(user.home, '.metro', 'bin', 'memory-routine');
|
|
34
|
+
export const skillPath = (user: AgentUser): string => join(user.home, '.claude', 'skills', 'memory', 'SKILL.md');
|
|
36
35
|
const logPath = (user: AgentUser): string => join(user.home, '.metro', 'memory-routine.log');
|
|
37
36
|
const quoted = (text: string): string => `'${text.replace(/'/g, '\'\\\'\'')}'`;
|
|
38
37
|
|
|
@@ -43,28 +42,38 @@ export function launcherText(user: AgentUser, cli: string, port: string): string
|
|
|
43
42
|
'export PATH',
|
|
44
43
|
...(port === '' ? [] : [`METRO_WEBHOOK_PORT=${quoted(port)}`, 'export METRO_WEBHOOK_PORT']),
|
|
45
44
|
'cd "$HOME" || exit 1',
|
|
46
|
-
`exec node ${quoted(cli)} memory`,
|
|
45
|
+
`exec node ${quoted(cli)} memory "$@"`,
|
|
47
46
|
'',
|
|
48
47
|
].join('\n');
|
|
49
48
|
}
|
|
50
49
|
|
|
51
|
-
export
|
|
52
|
-
const minute = parseInt(createHash('sha256').update(seed).digest('hex').slice(0, 8), 16) % 60;
|
|
53
|
-
return `${String(minute)} ${String(HOUR)} * * * ${launcherPath(user)} >> ${logPath(user)} 2>&1`;
|
|
54
|
-
}
|
|
50
|
+
export const cronLine = (user: AgentUser): string => `${SCHEDULE} ${launcherPath(user)} ${skillPath(user)} >> ${logPath(user)} 2>&1`;
|
|
55
51
|
|
|
56
52
|
const commandName = (command: string): string => command.split(/\s+/)[0]?.split('/').pop() ?? 'cron';
|
|
57
53
|
|
|
54
|
+
const ownCommand = (user: AgentUser, line: string): string | null => {
|
|
55
|
+
const parsed = parseCronLine(line);
|
|
56
|
+
return parsed === null || parsed.command.includes(launcherPath(user)) || !OWN_JOB.test(parsed.command) ? null : parsed.command;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const replaces = (user: AgentUser, line: string): boolean => OWN_JOB.test(commandName(ownCommand(user, line) ?? ''));
|
|
60
|
+
|
|
58
61
|
function ownJob(user: AgentUser, runner: Runner, lines: string[]): string | null {
|
|
59
|
-
const launcher = launcherPath(user);
|
|
60
62
|
const cron = lines.flatMap((line) => {
|
|
61
|
-
const
|
|
62
|
-
return
|
|
63
|
+
const command = ownCommand(user, line);
|
|
64
|
+
return command === null || replaces(user, line) ? [] : [commandName(command)];
|
|
63
65
|
});
|
|
64
66
|
const timers = listSchedules(null, runner).filter((job) => job.runsAs === user.name && OWN_JOB.test(`${job.name} ${job.command}`));
|
|
65
67
|
return cron[0] ?? timers[0]?.name ?? null;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
const restored = (line: string): string => (line.startsWith(REPLACED) ? line.slice(REPLACED.length) : line);
|
|
71
|
+
|
|
72
|
+
function marked(user: AgentUser, lines: string[], on: boolean, scheduled: boolean): string[] {
|
|
73
|
+
if (!on) return lines.map(restored);
|
|
74
|
+
return lines.map((l) => (scheduled && replaces(user, l) ? `${REPLACED}${l}` : l));
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
function placeLauncher(path: string, text: string): void {
|
|
69
78
|
if (existsSync(path) && readFileSync(path, 'utf8') === text) return;
|
|
70
79
|
writeHomeText(path, text, 0o755);
|
|
@@ -78,7 +87,6 @@ function resolved(deps: MemoryJobDeps): Required<MemoryJobDeps> {
|
|
|
78
87
|
runner: deps.runner ?? realRunner,
|
|
79
88
|
cli: deps.cli ?? envText('METRO_CLI_BIN'),
|
|
80
89
|
port: deps.port ?? envText('METRO_WEBHOOK_PORT'),
|
|
81
|
-
seed: deps.seed ?? hostname(),
|
|
82
90
|
};
|
|
83
91
|
}
|
|
84
92
|
|
|
@@ -88,7 +96,7 @@ const stateOf = (scheduled: boolean, own: string | null): MemoryJobState => {
|
|
|
88
96
|
};
|
|
89
97
|
|
|
90
98
|
export function ensureMemoryJob(on: boolean, deps: MemoryJobDeps = {}): MemoryJob {
|
|
91
|
-
const { user, runner, cli, port
|
|
99
|
+
const { user, runner, cli, port } = resolved(deps);
|
|
92
100
|
if (user === null || cli === '') {
|
|
93
101
|
last = { state: 'unavailable', job: null };
|
|
94
102
|
return memoryJobStatus();
|
|
@@ -98,10 +106,11 @@ export function ensureMemoryJob(on: boolean, deps: MemoryJobDeps = {}): MemoryJo
|
|
|
98
106
|
const scheduled = on && own === null;
|
|
99
107
|
const launcher = launcherPath(user);
|
|
100
108
|
if (scheduled) placeLauncher(launcher, launcherText(user, cli, port));
|
|
101
|
-
const
|
|
109
|
+
const kept = marked(user, lines, on, scheduled);
|
|
110
|
+
const next = withBlock(kept, BEGIN, END, scheduled ? [cronLine(user)] : [], 'end', (l) => l.includes(launcher));
|
|
102
111
|
const changed = writeCrontab(user, runner, lines, next);
|
|
103
112
|
last = { state: stateOf(scheduled, own), job: own };
|
|
104
|
-
if (changed) log.info({ ...last }, 'memory-routine: updated the
|
|
113
|
+
if (changed) log.info({ ...last, rewritten: kept.filter((l, i) => l !== lines[i]) }, 'memory-routine: updated the memory job');
|
|
105
114
|
return memoryJobStatus();
|
|
106
115
|
}
|
|
107
116
|
|
|
@@ -166,7 +166,7 @@ const PRIOR_STAGE: ReadonlySet<string> = new Set([
|
|
|
166
166
|
'e3b8f9bbdb205311bc643aaf91d9d903a91204fd5b55ca949b85cdfa6fd00136',
|
|
167
167
|
'08a30f5cd5cf8013ab70c1fa9e1109018c0337552296a8efe5c0cf543b284991',
|
|
168
168
|
]);
|
|
169
|
-
const PRIOR_MEMORY: ReadonlySet<string> = new Set();
|
|
169
|
+
const PRIOR_MEMORY: ReadonlySet<string> = new Set(['08795399635c8cdec2b50cb4e336f89ae880f3997ae52560ef647a9cdc8f1b08']);
|
|
170
170
|
|
|
171
171
|
interface ShippedSkill {
|
|
172
172
|
name: string;
|
package/runtime/runtime.json
CHANGED