0agent 1.0.17 → 1.0.19

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.
Files changed (3) hide show
  1. package/bin/0agent.js +324 -78
  2. package/dist/daemon.mjs +492 -36
  3. package/package.json +2 -1
package/bin/0agent.js CHANGED
@@ -98,26 +98,72 @@ switch (cmd) {
98
98
  await runWatch();
99
99
  break;
100
100
 
101
+ case 'memory':
102
+ await runMemoryCommand(args.slice(1));
103
+ break;
104
+
101
105
  default:
102
106
  showHelp();
103
107
  break;
104
108
  }
105
109
 
106
- // ─── Init wizard ─────────────────────────────────────────────────────────
110
+ // ─── Init wizard — arrow key selection, GitHub memory built in ───────────────
111
+
112
+ // Arrow-key select using enquirer (falls back to number input if not available)
113
+ async function arrowSelect(message, choices, initial = 0) {
114
+ try {
115
+ const mod = await import('enquirer');
116
+ const Enquirer = mod.default ?? mod;
117
+ const prompt = new Enquirer.Select({
118
+ message,
119
+ choices: choices.map((c, i) => ({ name: c, value: String(i) })),
120
+ initial,
121
+ });
122
+ const answer = await prompt.run();
123
+ // answer is the name string — find its index
124
+ const idx = choices.indexOf(answer);
125
+ return idx >= 0 ? idx : initial;
126
+ } catch {
127
+ // Fallback: number-based selection if enquirer unavailable or non-TTY
128
+ return choose(message, choices, initial);
129
+ }
130
+ }
131
+
132
+ async function arrowInput(message, initial = '') {
133
+ try {
134
+ const mod = await import('enquirer');
135
+ const Enquirer = mod.default ?? mod;
136
+ const prompt = new Enquirer.Input({ message, initial });
137
+ return await prompt.run();
138
+ } catch {
139
+ return ask(` ${message}: `);
140
+ }
141
+ }
142
+
143
+ async function arrowPassword(message) {
144
+ try {
145
+ const mod = await import('enquirer');
146
+ const Enquirer = mod.default ?? mod;
147
+ const prompt = new Enquirer.Password({ message });
148
+ return await prompt.run();
149
+ } catch {
150
+ return ask(` ${message}: `);
151
+ }
152
+ }
107
153
 
108
154
  async function runInit() {
109
- console.log('\n ┌─────────────────────────────────────────┐');
110
- console.log(' │ ');
111
- console.log(' │ 0agent — An agent that learns. ');
112
- console.log(' │ ');
113
- console.log(' │ v1.0.0 · Apache 2.0');
114
- console.log(' └─────────────────────────────────────────┘\n');
115
-
116
- // Check if already initialised
155
+ console.log('\n \x1b[1m┌─────────────────────────────────────────┐\x1b[0m');
156
+ console.log(' \x1b[1m│\x1b[0m');
157
+ console.log(' \x1b[1m│ 0agent — An agent that learns. │\x1b[0m');
158
+ console.log(' \x1b[1m│\x1b[0m');
159
+ console.log(' \x1b[1m│ v1.0 · Apache 2.0 │\x1b[0m');
160
+ console.log(' \x1b[1m└─────────────────────────────────────────┘\x1b[0m\n');
161
+
117
162
  if (existsSync(CONFIG_PATH)) {
118
- const answer = await ask('Config already exists. Reinitialise? [y/N] ');
163
+ const answer = await ask(' Config already exists. Reinitialise? [y/N] ');
119
164
  if (answer.toLowerCase() !== 'y') {
120
- console.log('\n Run `0agent start` to start the daemon.\n');
165
+ console.log('\n Running: 0agent start\n');
166
+ await startDaemon();
121
167
  return;
122
168
  }
123
169
  }
@@ -127,96 +173,110 @@ async function runInit() {
127
173
  mkdirSync(resolve(AGENT_DIR, 'skills', 'builtin'), { recursive: true });
128
174
  mkdirSync(resolve(AGENT_DIR, 'skills', 'custom'), { recursive: true });
129
175
 
130
- console.log(' Step 1 of 5: LLM Provider\n');
131
- const provider = await choose(' Which LLM provider?', [
176
+ // ── Step 1: LLM Provider ────────────────────────────────────────────────
177
+ const providerIdx = await arrowSelect('LLM Provider', [
132
178
  'Anthropic (Claude) ← recommended',
133
179
  'OpenAI (GPT-4o)',
134
180
  'xAI (Grok)',
135
181
  'Google (Gemini)',
136
- 'Ollama (local, free)',
182
+ 'Ollama (local — no API key)',
137
183
  ], 0);
138
- const providerKey = ['anthropic', 'openai', 'xai', 'gemini', 'ollama'][provider];
184
+ const providerKey = ['anthropic', 'openai', 'xai', 'gemini', 'ollama'][providerIdx];
139
185
 
140
- // Model selection per provider
141
186
  const MODELS = {
142
- anthropic: [
143
- 'claude-sonnet-4-6 ← recommended (fast + smart)',
144
- 'claude-opus-4-6 (most capable, slower)',
145
- 'claude-haiku-4-5 (fastest, cheapest)',
146
- ],
147
- openai: [
148
- 'gpt-4o ← recommended',
149
- 'gpt-4o-mini (faster, cheaper)',
150
- 'o3-mini (reasoning)',
151
- ],
152
- xai: [
153
- 'grok-3 ← recommended',
154
- 'grok-3-mini',
155
- ],
156
- gemini: [
157
- 'gemini-2.0-flash ← recommended',
158
- 'gemini-2.0-pro',
159
- ],
160
- ollama: [
161
- 'llama3.1 ← recommended',
162
- 'mistral',
163
- 'codellama',
164
- ],
187
+ anthropic: ['claude-sonnet-4-6 ← recommended', 'claude-opus-4-6 (most capable)', 'claude-haiku-4-5 (fastest)'],
188
+ openai: ['gpt-4o ← recommended', 'gpt-4o-mini', 'o3-mini'],
189
+ xai: ['grok-3 ← recommended', 'grok-3-mini'],
190
+ gemini: ['gemini-2.0-flash ← recommended', 'gemini-2.0-pro'],
191
+ ollama: ['llama3.1 ← recommended', 'mistral', 'codellama'],
165
192
  };
166
-
167
- let model = '';
168
- if (MODELS[providerKey]) {
169
- console.log();
170
- const modelIdx = await choose(' Which model?', MODELS[providerKey], 0);
171
- model = MODELS[providerKey][modelIdx].split(/\s+/)[0];
172
- }
193
+ const modelIdx = await arrowSelect('Which model?', MODELS[providerKey], 0);
194
+ const model = MODELS[providerKey][modelIdx].split(/\s+/)[0];
173
195
 
174
196
  let apiKey = '';
175
197
  if (providerKey !== 'ollama') {
176
- apiKey = await ask(`\n API Key: `);
177
- if (!apiKey.trim()) {
178
- console.log(' ⚠️ No API key provided. You can set it later in ~/.0agent/config.yaml');
198
+ apiKey = await arrowPassword(`${providerKey} API key`);
199
+ apiKey = apiKey.trim();
200
+ if (!apiKey) {
201
+ console.log(' \x1b[33m⚠\x1b[0m No key — add it later in ~/.0agent/config.yaml');
179
202
  } else {
180
- // Validate key format
181
- const keyPrefixes = { anthropic: 'sk-ant-', openai: 'sk-', xai: 'xai-', gemini: 'AI' };
182
- const expectedPrefix = keyPrefixes[providerKey];
183
- if (expectedPrefix && !apiKey.startsWith(expectedPrefix)) {
184
- console.log(` ⚠️ Key doesn't look like a ${providerKey} key (expected: ${expectedPrefix}...)`);
203
+ const pfx = { anthropic: 'sk-ant-', openai: 'sk-', xai: 'xai-', gemini: 'AI' }[providerKey];
204
+ if (pfx && !apiKey.startsWith(pfx)) {
205
+ console.log(` \x1b[33m⚠\x1b[0m Unexpected key format (expected ${pfx}...)`);
185
206
  } else {
186
- console.log(' API key format looks valid');
207
+ console.log(' \x1b[32m✓\x1b[0m Key format valid');
208
+ }
209
+ }
210
+ }
211
+
212
+ // ── Step 2: GitHub Memory ───────────────────────────────────────────────
213
+ const memChoice = await arrowSelect('Back up memory to GitHub?', [
214
+ 'Yes — private repo, free, cross-device sync ← recommended',
215
+ 'No — local only',
216
+ ], 0);
217
+
218
+ let ghToken = '', ghOwner = '', ghRepo = '0agent-memory';
219
+ if (memChoice === 0) {
220
+ // Try gh CLI first
221
+ try {
222
+ const { execSync: ex } = await import('node:child_process');
223
+ ghToken = ex('gh auth token 2>/dev/null', { encoding: 'utf8' }).trim();
224
+ ghOwner = ex('gh api user --jq .login 2>/dev/null', { encoding: 'utf8' }).trim();
225
+ if (ghToken && ghOwner) {
226
+ console.log(` \x1b[32m✓\x1b[0m gh CLI — authenticated as \x1b[1m${ghOwner}\x1b[0m`);
227
+ }
228
+ } catch {}
229
+
230
+ if (!ghToken) {
231
+ console.log('\n Create a GitHub token: \x1b[4mhttps://github.com/settings/tokens/new\x1b[0m');
232
+ console.log(' Required scope: \x1b[1mrepo\x1b[0m\n');
233
+ ghToken = await arrowPassword('GitHub token (ghp_...)');
234
+ ghToken = ghToken.trim();
235
+ if (ghToken) {
236
+ ghOwner = await verifyGitHubToken(ghToken) ?? '';
237
+ if (!ghOwner) {
238
+ console.log(' \x1b[31m✗\x1b[0m Invalid token — skipping GitHub memory');
239
+ ghToken = '';
240
+ } else {
241
+ console.log(` \x1b[32m✓\x1b[0m Authenticated as \x1b[1m${ghOwner}\x1b[0m`);
242
+ }
187
243
  }
188
244
  }
245
+
246
+ if (ghToken && ghOwner) {
247
+ process.stdout.write(` Creating private repo \x1b[1m${ghOwner}/0agent-memory\x1b[0m...`);
248
+ const ok = await createGitHubRepo(ghToken, '0agent-memory');
249
+ console.log(ok ? ' \x1b[32m✓\x1b[0m' : ' \x1b[33m(exists)\x1b[0m');
250
+ }
189
251
  }
190
252
 
191
- console.log('\n Step 2 of 5: Embedding model\n');
192
- const embedding = await choose(' Embedding backend?', [
193
- 'Local via Ollama (nomic-embed-text) free, private',
194
- 'OpenAI text-embedding-3-small (cloud)',
195
- 'Skip (text-only mode)',
253
+ // ── Step 3: Embedding ────────────────────────────────────────────────────
254
+ const embIdx = await arrowSelect('Embeddings (for semantic memory search)?', [
255
+ 'Local via Ollama (nomic-embed-text) free, private',
256
+ 'OpenAI text-embedding-3-small cloud',
257
+ 'Skip text-only mode',
196
258
  ], 0);
197
- const embeddingProvider = ['nomic-ollama', 'openai', 'none'][embedding];
259
+ const embeddingProvider = ['nomic-ollama', 'openai', 'none'][embIdx];
198
260
 
199
- console.log('\n Step 3 of 5: Sandbox backend\n');
261
+ // ── Step 4: Sandbox ──────────────────────────────────────────────────────
200
262
  const sandboxes = detectSandboxes();
201
- console.log(` Detected: ${sandboxes.join(', ') || 'process (fallback)'}`);
202
263
  const sandboxChoice = sandboxes[0] ?? 'process';
203
- console.log(` Using: ${sandboxChoice}`);
264
+ console.log(`\n Sandbox: \x1b[32m${sandboxChoice}\x1b[0m detected`);
204
265
 
205
- console.log('\n Step 4 of 5: Seed graph\n');
206
- const seed = await choose(' Start with a seed graph?', [
207
- 'software-engineering (skills + sprint workflow) ← recommended',
208
- 'scratch (empty graph)',
266
+ // ── Step 5: Seed graph ───────────────────────────────────────────────────
267
+ const seedIdx = await arrowSelect('Starting knowledge?', [
268
+ 'software-engineering sprint workflow + 15 skills ← recommended',
269
+ 'Start from scratch',
209
270
  ], 0);
210
- const seedName = seed === 0 ? 'software-engineering' : null;
211
-
212
- // Step 5: confirm
213
- console.log('\n Step 5 of 5: Ready\n');
214
- console.log(` Provider: ${providerKey}`);
215
- console.log(` Model: ${model}`);
216
- console.log(` API Key: ${apiKey ? apiKey.slice(0, 8) + '••••••••' : '(not set)'}`);
217
- console.log(` Sandbox: ${sandboxChoice}`);
218
- console.log(` Seed: ${seedName ?? 'scratch'}`);
219
- console.log();
271
+ const seedName = seedIdx === 0 ? 'software-engineering' : null;
272
+
273
+ // ── Summary ───────────────────────────────────────────────────────────────
274
+ console.log('\n \x1b[1mReady to launch\x1b[0m\n');
275
+ console.log(` LLM: \x1b[36m${providerKey}/${model}\x1b[0m`);
276
+ console.log(` API Key: ${apiKey ? '\x1b[32m✓ set\x1b[0m (' + apiKey.slice(0, 8) + '••••)' : '\x1b[33mnot set\x1b[0m'}`);
277
+ console.log(` Memory: ${ghToken ? `\x1b[32mgithub.com/${ghOwner}/0agent-memory\x1b[0m` : '\x1b[2mlocal only\x1b[0m'}`);
278
+ console.log(` Sandbox: \x1b[36m${sandboxChoice}\x1b[0m`);
279
+ console.log(` Seed: \x1b[36m${seedName ?? 'scratch'}\x1b[0m\n`);
220
280
 
221
281
  // Write config
222
282
  const dbPath = resolve(AGENT_DIR, 'graph.db');
@@ -252,6 +312,7 @@ graph:
252
312
  hnsw_path: "${hnswPath}"
253
313
  object_store_path: "${objPath}"
254
314
  ${seedName ? `\nseed: "${seedName}"` : ''}
315
+ ${ghToken && ghOwner ? `\ngithub_memory:\n enabled: true\n token: "${ghToken}"\n owner: "${ghOwner}"\n repo: "${ghRepo}"` : ''}
255
316
  `;
256
317
 
257
318
  writeFileSync(CONFIG_PATH, config, 'utf8');
@@ -809,6 +870,191 @@ async function waitForTunnelUrl(proc, pattern, timeout) {
809
870
  });
810
871
  }
811
872
 
873
+ // ─── Memory sync (GitHub backend) ─────────────────────────────────────────
874
+
875
+ async function runMemoryCommand(memArgs) {
876
+ const sub = memArgs[0] ?? 'status';
877
+
878
+ switch (sub) {
879
+ // ── 0agent memory connect github ──────────────────────────────────────
880
+ case 'connect': {
881
+ const provider = memArgs[1] ?? 'github';
882
+ if (provider !== 'github') { console.log(' Only GitHub is supported: 0agent memory connect github'); break; }
883
+
884
+ console.log('\n \x1b[1m0agent Memory — GitHub Sync\x1b[0m\n');
885
+ console.log(' Your knowledge graph will be backed up to a private GitHub repository.');
886
+ console.log(' Free, versioned, cross-device. No server needed.\n');
887
+
888
+ // ── Authentication ──
889
+ let token = '';
890
+
891
+ // Try gh CLI first (already logged in for most devs)
892
+ try {
893
+ const { execSync: ex } = await import('node:child_process');
894
+ token = ex('gh auth token 2>/dev/null', { encoding: 'utf8' }).trim();
895
+ if (token) {
896
+ const { execSync: ex2 } = await import('node:child_process');
897
+ const user = ex2('gh api user --jq .login 2>/dev/null', { encoding: 'utf8' }).trim();
898
+ console.log(` \x1b[32m✓\x1b[0m Detected gh CLI — authenticated as \x1b[1m${user}\x1b[0m`);
899
+ }
900
+ } catch {}
901
+
902
+ // Fallback: GitHub Device Flow
903
+ if (!token) {
904
+ console.log(' \x1b[2mgh CLI not found — using GitHub token auth\x1b[0m\n');
905
+ console.log(' Create a token at: \x1b[4mhttps://github.com/settings/tokens/new\x1b[0m');
906
+ console.log(' Required scope: \x1b[1mrepo\x1b[0m\n');
907
+ token = await ask(' Paste token (starts with ghp_): ');
908
+ token = token.trim();
909
+ if (!token) { console.log(' No token provided.'); break; }
910
+ }
911
+
912
+ // Verify token
913
+ const owner = await verifyGitHubToken(token);
914
+ if (!owner) { console.log(' \x1b[31m✗\x1b[0m Invalid token or no access.'); break; }
915
+ console.log(` \x1b[32m✓\x1b[0m Authenticated as \x1b[1m${owner}\x1b[0m`);
916
+
917
+ // ── Create repo ──
918
+ const repoName = memArgs[2] ?? '0agent-memory';
919
+ process.stdout.write(` Creating private repo \x1b[1m${owner}/${repoName}\x1b[0m...`);
920
+ const created = await createGitHubRepo(token, repoName);
921
+ console.log(created ? ' \x1b[32m✓\x1b[0m' : ' \x1b[33m(already exists)\x1b[0m');
922
+
923
+ // ── Save to config ──
924
+ const YAML = await import('yaml');
925
+ const { readFileSync: rf, writeFileSync: wf, existsSync: ef } = await import('node:fs');
926
+ if (ef(CONFIG_PATH)) {
927
+ let cfg = rf(CONFIG_PATH, 'utf8');
928
+ // Remove old github_memory block if present
929
+ cfg = cfg.replace(/\ngithub_memory:[\s\S]*?(?=\n\w|\n$|$)/, '');
930
+ cfg += `\ngithub_memory:\n enabled: true\n token: "${token}"\n owner: "${owner}"\n repo: "${repoName}"\n`;
931
+ wf(CONFIG_PATH, cfg, 'utf8');
932
+ }
933
+
934
+ // ── Initial push ──
935
+ console.log('\n Performing initial sync...');
936
+ await requireDaemon();
937
+ const result = await daemonMemorySync('push');
938
+ if (result?.pushed) {
939
+ console.log(` \x1b[32m✓\x1b[0m Synced — ${result.nodes_synced} nodes, ${result.edges_synced} edges`);
940
+ console.log(`\n Memory repo: \x1b[4mhttps://github.com/${owner}/${repoName}\x1b[0m`);
941
+ console.log('\n From any machine, run:');
942
+ console.log(` \x1b[36m0agent memory connect github --repo ${owner}/${repoName}\x1b[0m\n`);
943
+ } else {
944
+ console.log(' \x1b[33m⚠\x1b[0m Initial sync skipped — run `0agent memory sync` after daemon starts.');
945
+ console.log(`\n Memory repo: \x1b[4mhttps://github.com/${owner}/${repoName}\x1b[0m\n`);
946
+ }
947
+ break;
948
+ }
949
+
950
+ // ── 0agent memory sync ────────────────────────────────────────────────
951
+ case 'sync': {
952
+ await requireDaemon();
953
+ process.stdout.write(' Syncing memory to GitHub...');
954
+ const result = await daemonMemorySync('push');
955
+ if (result?.pushed) {
956
+ console.log(` \x1b[32m✓\x1b[0m ${result.nodes_synced} nodes, ${result.edges_synced} edges`);
957
+ } else {
958
+ console.log(` \x1b[31m✗\x1b[0m ${result?.error ?? 'No GitHub memory configured'}`);
959
+ if (!result?.error) console.log(' Run: 0agent memory connect github');
960
+ }
961
+ break;
962
+ }
963
+
964
+ // ── 0agent memory pull ───────────────────────────────────────────────
965
+ case 'pull': {
966
+ await requireDaemon();
967
+ process.stdout.write(' Pulling memory from GitHub...');
968
+ const result = await daemonMemorySync('pull');
969
+ if (result?.pulled) {
970
+ console.log(` \x1b[32m✓\x1b[0m +${result.nodes_synced} nodes, +${result.edges_synced} edges merged`);
971
+ } else {
972
+ console.log(` \x1b[31m✗\x1b[0m ${result?.error ?? 'No GitHub memory configured'}`);
973
+ }
974
+ break;
975
+ }
976
+
977
+ // ── 0agent memory status ──────────────────────────────────────────────
978
+ case 'status': {
979
+ const YAML = await import('yaml');
980
+ const { readFileSync: rf, existsSync: ef } = await import('node:fs');
981
+ if (!ef(CONFIG_PATH)) { console.log('\n Not initialised. Run: 0agent init\n'); break; }
982
+
983
+ const cfg = YAML.parse(rf(CONFIG_PATH, 'utf8'));
984
+ const ghMem = cfg.github_memory;
985
+
986
+ if (!ghMem?.enabled) {
987
+ console.log('\n Memory sync: \x1b[33mnot connected\x1b[0m');
988
+ console.log(' Run: 0agent memory connect github\n');
989
+ } else {
990
+ console.log(`\n Memory sync: \x1b[32m✓ connected\x1b[0m`);
991
+ console.log(` Repo: https://github.com/${ghMem.owner}/${ghMem.repo}`);
992
+ // Get last sync from daemon
993
+ try {
994
+ const res = await fetch(`${BASE_URL}/api/memory/status`).catch(() => null);
995
+ const data = res?.ok ? await res.json() : null;
996
+ if (data) {
997
+ console.log(` Last push: ${data.pushed_at ? new Date(data.pushed_at).toLocaleString() : 'never'}`);
998
+ console.log(` Last pull: ${data.pulled_at ? new Date(data.pulled_at).toLocaleString() : 'never'}`);
999
+ }
1000
+ } catch {}
1001
+ console.log();
1002
+ }
1003
+ break;
1004
+ }
1005
+
1006
+ // ── 0agent memory disconnect ──────────────────────────────────────────
1007
+ case 'disconnect': {
1008
+ const { readFileSync: rf, writeFileSync: wf, existsSync: ef } = await import('node:fs');
1009
+ if (ef(CONFIG_PATH)) {
1010
+ let cfg = rf(CONFIG_PATH, 'utf8');
1011
+ cfg = cfg.replace(/\ngithub_memory:[\s\S]*?(?=\n\w|\n$|$)/, '');
1012
+ wf(CONFIG_PATH, cfg, 'utf8');
1013
+ }
1014
+ console.log(' \x1b[32m✓\x1b[0m GitHub memory sync disabled. Local graph unchanged.');
1015
+ break;
1016
+ }
1017
+
1018
+ default:
1019
+ console.log(' Usage: 0agent memory connect github | sync | pull | status | disconnect');
1020
+ }
1021
+ }
1022
+
1023
+ async function verifyGitHubToken(token) {
1024
+ try {
1025
+ const res = await fetch('https://api.github.com/user', {
1026
+ headers: { Authorization: `Bearer ${token}`, 'User-Agent': '0agent/1.0' },
1027
+ signal: AbortSignal.timeout(8000),
1028
+ });
1029
+ if (!res.ok) return null;
1030
+ const user = await res.json();
1031
+ return user.login;
1032
+ } catch { return null; }
1033
+ }
1034
+
1035
+ async function createGitHubRepo(token, repoName) {
1036
+ const res = await fetch('https://api.github.com/user/repos', {
1037
+ method: 'POST',
1038
+ headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json', 'User-Agent': '0agent/1.0' },
1039
+ body: JSON.stringify({
1040
+ name: repoName,
1041
+ description: '0agent memory — knowledge graph backed up automatically',
1042
+ private: true,
1043
+ auto_init: true,
1044
+ }),
1045
+ signal: AbortSignal.timeout(10000),
1046
+ });
1047
+ return res.ok || res.status === 422; // 422 = already exists
1048
+ }
1049
+
1050
+ async function daemonMemorySync(direction) {
1051
+ try {
1052
+ const res = await fetch(`${BASE_URL}/api/memory/${direction}`, { method: 'POST' });
1053
+ if (!res.ok) return null;
1054
+ return await res.json();
1055
+ } catch { return null; }
1056
+ }
1057
+
812
1058
  // ─── Result preview — confirms the agent's work actually ran ────────────────
813
1059
 
814
1060
  async function showResultPreview(result) {
package/dist/daemon.mjs CHANGED
@@ -1490,7 +1490,7 @@ var init_EdgeWeightUpdater = __esm({
1490
1490
  this.weightLog.append(event);
1491
1491
  }
1492
1492
  sleep(ms) {
1493
- return new Promise((resolve11) => setTimeout(resolve11, ms));
1493
+ return new Promise((resolve12) => setTimeout(resolve12, ms));
1494
1494
  }
1495
1495
  };
1496
1496
  }
@@ -2613,7 +2613,7 @@ var init_AgentExecutor = __esm({
2613
2613
  }
2614
2614
  }
2615
2615
  shellExec(command, timeoutMs) {
2616
- return new Promise((resolve11) => {
2616
+ return new Promise((resolve12) => {
2617
2617
  const chunks = [];
2618
2618
  const proc = spawn2("bash", ["-c", command], {
2619
2619
  cwd: this.cwd,
@@ -2624,10 +2624,10 @@ var init_AgentExecutor = __esm({
2624
2624
  proc.stderr.on("data", (d) => chunks.push(d.toString()));
2625
2625
  proc.on("close", (code) => {
2626
2626
  const output = chunks.join("").trim();
2627
- resolve11(output || (code === 0 ? "(command completed, no output)" : `exit code ${code}`));
2627
+ resolve12(output || (code === 0 ? "(command completed, no output)" : `exit code ${code}`));
2628
2628
  });
2629
2629
  proc.on("error", (err) => {
2630
- resolve11(`Error: ${err.message}`);
2630
+ resolve12(`Error: ${err.message}`);
2631
2631
  });
2632
2632
  });
2633
2633
  }
@@ -2933,11 +2933,11 @@ __export(ProactiveSurface_exports, {
2933
2933
  ProactiveSurface: () => ProactiveSurface
2934
2934
  });
2935
2935
  import { execSync as execSync4 } from "node:child_process";
2936
- import { existsSync as existsSync9, readFileSync as readFileSync9, statSync, readdirSync as readdirSync4 } from "node:fs";
2937
- import { resolve as resolve8, join as join3 } from "node:path";
2936
+ import { existsSync as existsSync10, readFileSync as readFileSync10, statSync, readdirSync as readdirSync5 } from "node:fs";
2937
+ import { resolve as resolve9, join as join3 } from "node:path";
2938
2938
  function readdirSafe(dir) {
2939
2939
  try {
2940
- return readdirSync4(dir);
2940
+ return readdirSync5(dir);
2941
2941
  } catch {
2942
2942
  return [];
2943
2943
  }
@@ -2983,7 +2983,7 @@ var init_ProactiveSurface = __esm({
2983
2983
  return [...this.insights];
2984
2984
  }
2985
2985
  async poll() {
2986
- if (!existsSync9(resolve8(this.cwd, ".git"))) return;
2986
+ if (!existsSync10(resolve9(this.cwd, ".git"))) return;
2987
2987
  const newInsights = [];
2988
2988
  const gitInsight = this.checkGitActivity();
2989
2989
  if (gitInsight) newInsights.push(gitInsight);
@@ -3027,13 +3027,13 @@ var init_ProactiveSurface = __esm({
3027
3027
  ];
3028
3028
  for (const dir of outputPaths) {
3029
3029
  try {
3030
- if (!existsSync9(dir)) continue;
3030
+ if (!existsSync10(dir)) continue;
3031
3031
  const xmlFiles = readdirSafe(dir).filter((f) => f.endsWith(".xml"));
3032
3032
  for (const xml of xmlFiles) {
3033
3033
  const path = join3(dir, xml);
3034
3034
  const stat = statSync(path);
3035
3035
  if (stat.mtimeMs < this.lastPollAt) continue;
3036
- const content = readFileSync9(path, "utf8");
3036
+ const content = readFileSync10(path, "utf8");
3037
3037
  const failures = [...content.matchAll(/<failure[^>]*message="([^"]+)"/g)].length;
3038
3038
  if (failures > 0) {
3039
3039
  return this.makeInsight(
@@ -3088,9 +3088,9 @@ var init_ProactiveSurface = __esm({
3088
3088
 
3089
3089
  // packages/daemon/src/ZeroAgentDaemon.ts
3090
3090
  init_src();
3091
- import { writeFileSync as writeFileSync6, unlinkSync as unlinkSync2, existsSync as existsSync10, mkdirSync as mkdirSync6 } from "node:fs";
3092
- import { resolve as resolve9 } from "node:path";
3093
- import { homedir as homedir5 } from "node:os";
3091
+ import { writeFileSync as writeFileSync7, unlinkSync as unlinkSync2, existsSync as existsSync11, mkdirSync as mkdirSync6 } from "node:fs";
3092
+ import { resolve as resolve10 } from "node:path";
3093
+ import { homedir as homedir6 } from "node:os";
3094
3094
 
3095
3095
  // packages/daemon/src/config/DaemonConfig.ts
3096
3096
  import { readFileSync, existsSync } from "node:fs";
@@ -3199,7 +3199,13 @@ var DaemonConfigSchema = z.object({
3199
3199
  decay: DecayConfigSchema.default({}),
3200
3200
  seed: z.string().optional(),
3201
3201
  self_improvement: SelfImprovementConfigSchema.default({}),
3202
- entity_nesting: EntityNestingConfigSchema.default({})
3202
+ entity_nesting: EntityNestingConfigSchema.default({}),
3203
+ github_memory: z.object({
3204
+ enabled: z.boolean().default(false),
3205
+ token: z.string().default(""),
3206
+ owner: z.string().default(""),
3207
+ repo: z.string().default("0agent-memory")
3208
+ }).default({})
3203
3209
  });
3204
3210
 
3205
3211
  // packages/daemon/src/config/DaemonConfig.ts
@@ -3536,19 +3542,19 @@ var ProjectScanner = class {
3536
3542
  async getRunningPorts() {
3537
3543
  const open = [];
3538
3544
  await Promise.all(PORTS_TO_CHECK.map(
3539
- (port) => new Promise((resolve11) => {
3545
+ (port) => new Promise((resolve12) => {
3540
3546
  const s = createServer();
3541
3547
  s.listen(port, "127.0.0.1", () => {
3542
3548
  s.close();
3543
- resolve11();
3549
+ resolve12();
3544
3550
  });
3545
3551
  s.on("error", () => {
3546
3552
  open.push(port);
3547
- resolve11();
3553
+ resolve12();
3548
3554
  });
3549
3555
  setTimeout(() => {
3550
3556
  s.close();
3551
- resolve11();
3557
+ resolve12();
3552
3558
  }, 200);
3553
3559
  })
3554
3560
  ));
@@ -4291,7 +4297,7 @@ var SkillRegistry = class {
4291
4297
  };
4292
4298
 
4293
4299
  // packages/daemon/src/HTTPServer.ts
4294
- import { Hono as Hono8 } from "hono";
4300
+ import { Hono as Hono10 } from "hono";
4295
4301
  import { serve } from "@hono/node-server";
4296
4302
  import { readFileSync as readFileSync6 } from "node:fs";
4297
4303
  import { resolve as resolve5, dirname as dirname3 } from "node:path";
@@ -4546,6 +4552,48 @@ function skillRoutes(deps) {
4546
4552
  return app;
4547
4553
  }
4548
4554
 
4555
+ // packages/daemon/src/routes/insights.ts
4556
+ import { Hono as Hono8 } from "hono";
4557
+ function insightsRoutes(deps) {
4558
+ const app = new Hono8();
4559
+ app.get("/", (c) => {
4560
+ if (!deps.proactiveSurface) return c.json([]);
4561
+ const seen = c.req.query("seen");
4562
+ const insights = seen === "false" ? deps.proactiveSurface.getUnseen() : deps.proactiveSurface.getAll();
4563
+ return c.json(insights);
4564
+ });
4565
+ app.post("/:id/seen", (c) => {
4566
+ if (!deps.proactiveSurface) return c.json({ ok: false, error: "not available" }, 404);
4567
+ deps.proactiveSurface.markSeen(c.req.param("id"));
4568
+ return c.json({ ok: true });
4569
+ });
4570
+ return app;
4571
+ }
4572
+
4573
+ // packages/daemon/src/routes/memory.ts
4574
+ import { Hono as Hono9 } from "hono";
4575
+ function memoryRoutes(deps) {
4576
+ const app = new Hono9();
4577
+ app.post("/push", async (c) => {
4578
+ const sync = deps.getSync();
4579
+ if (!sync) return c.json({ error: "GitHub memory not configured. Run: 0agent memory connect github" }, 404);
4580
+ const result = await sync.push();
4581
+ return c.json(result);
4582
+ });
4583
+ app.post("/pull", async (c) => {
4584
+ const sync = deps.getSync();
4585
+ if (!sync) return c.json({ error: "GitHub memory not configured." }, 404);
4586
+ const result = await sync.pull();
4587
+ return c.json(result);
4588
+ });
4589
+ app.get("/status", (c) => {
4590
+ const sync = deps.getSync();
4591
+ if (!sync) return c.json({ configured: false });
4592
+ return c.json({ configured: true, ...sync.getLastSyncTimes() });
4593
+ });
4594
+ return app;
4595
+ }
4596
+
4549
4597
  // packages/daemon/src/HTTPServer.ts
4550
4598
  function findGraphHtml() {
4551
4599
  const candidates = [
@@ -4571,7 +4619,7 @@ var HTTPServer = class {
4571
4619
  deps;
4572
4620
  constructor(deps) {
4573
4621
  this.deps = deps;
4574
- this.app = new Hono8();
4622
+ this.app = new Hono10();
4575
4623
  this.app.route("/api/health", healthRoutes({ getStatus: deps.getStatus }));
4576
4624
  this.app.route("/api/sessions", sessionRoutes({ sessions: deps.sessions }));
4577
4625
  this.app.route("/api/graph", graphRoutes({ graph: deps.graph }));
@@ -4579,6 +4627,8 @@ var HTTPServer = class {
4579
4627
  this.app.route("/api/traces", traceRoutes({ traceStore: deps.traceStore }));
4580
4628
  this.app.route("/api/subagents", subagentRoutes());
4581
4629
  this.app.route("/api/skills", skillRoutes({ skillRegistry: deps.skillRegistry }));
4630
+ this.app.route("/api/insights", insightsRoutes({ proactiveSurface: deps.proactiveSurface ?? null }));
4631
+ this.app.route("/api/memory", memoryRoutes({ getSync: deps.getMemorySync ?? (() => null) }));
4582
4632
  const serveGraph = (c) => {
4583
4633
  try {
4584
4634
  const html = readFileSync6(GRAPH_HTML_PATH, "utf8");
@@ -4591,7 +4641,7 @@ var HTTPServer = class {
4591
4641
  this.app.get("/graph", serveGraph);
4592
4642
  }
4593
4643
  start() {
4594
- return new Promise((resolve11) => {
4644
+ return new Promise((resolve12) => {
4595
4645
  this.server = serve(
4596
4646
  {
4597
4647
  fetch: this.app.fetch,
@@ -4599,20 +4649,20 @@ var HTTPServer = class {
4599
4649
  hostname: this.deps.host
4600
4650
  },
4601
4651
  () => {
4602
- resolve11();
4652
+ resolve12();
4603
4653
  }
4604
4654
  );
4605
4655
  });
4606
4656
  }
4607
4657
  stop() {
4608
- return new Promise((resolve11, reject) => {
4658
+ return new Promise((resolve12, reject) => {
4609
4659
  if (!this.server) {
4610
- resolve11();
4660
+ resolve12();
4611
4661
  return;
4612
4662
  }
4613
4663
  this.server.close((err) => {
4614
4664
  if (err) reject(err);
4615
- else resolve11();
4665
+ else resolve12();
4616
4666
  });
4617
4667
  });
4618
4668
  }
@@ -5148,6 +5198,372 @@ var TeamSync = class {
5148
5198
  }
5149
5199
  };
5150
5200
 
5201
+ // packages/daemon/src/GitHubMemorySync.ts
5202
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync6, existsSync as existsSync9, readdirSync as readdirSync4 } from "node:fs";
5203
+ import { resolve as resolve8 } from "node:path";
5204
+ import { homedir as homedir5 } from "node:os";
5205
+ var GITHUB_API = "https://api.github.com";
5206
+ async function ghFetch(path, token, opts) {
5207
+ return fetch(`${GITHUB_API}${path}`, {
5208
+ ...opts,
5209
+ headers: {
5210
+ Authorization: `Bearer ${token}`,
5211
+ Accept: "application/vnd.github.v3+json",
5212
+ "Content-Type": "application/json",
5213
+ "User-Agent": "0agent/1.0",
5214
+ ...opts?.headers ?? {}
5215
+ },
5216
+ signal: AbortSignal.timeout(15e3)
5217
+ });
5218
+ }
5219
+ async function getFileSha(token, owner, repo, path) {
5220
+ try {
5221
+ const res = await ghFetch(`/repos/${owner}/${repo}/contents/${path}`, token);
5222
+ if (!res.ok) return null;
5223
+ const data = await res.json();
5224
+ return data.sha;
5225
+ } catch {
5226
+ return null;
5227
+ }
5228
+ }
5229
+ async function putFile(token, owner, repo, path, content, message) {
5230
+ const sha = await getFileSha(token, owner, repo, path);
5231
+ const body = {
5232
+ message,
5233
+ content: Buffer.from(content).toString("base64")
5234
+ };
5235
+ if (sha) body.sha = sha;
5236
+ const res = await ghFetch(`/repos/${owner}/${repo}/contents/${path}`, token, {
5237
+ method: "PUT",
5238
+ body: JSON.stringify(body)
5239
+ });
5240
+ return res.ok;
5241
+ }
5242
+ async function getFile(token, owner, repo, path) {
5243
+ try {
5244
+ const res = await ghFetch(`/repos/${owner}/${repo}/contents/${path}`, token);
5245
+ if (!res.ok) return null;
5246
+ const data = await res.json();
5247
+ return Buffer.from(data.content, "base64").toString("utf8");
5248
+ } catch {
5249
+ return null;
5250
+ }
5251
+ }
5252
+ var GitHubMemorySync = class {
5253
+ constructor(config, adapter, graph) {
5254
+ this.config = config;
5255
+ this.adapter = adapter;
5256
+ this.graph = graph;
5257
+ }
5258
+ lastPushAt = 0;
5259
+ lastPullAt = 0;
5260
+ pendingChanges = false;
5261
+ /**
5262
+ * Push current graph state to GitHub.
5263
+ * Called automatically every 30 minutes and on `0agent memory sync`.
5264
+ */
5265
+ async push(message) {
5266
+ const now = Date.now();
5267
+ const commitMsg = message ?? `sync: ${new Date(now).toISOString()}`;
5268
+ try {
5269
+ const nodes = this.adapter.queryNodes({});
5270
+ const nodesJson = JSON.stringify(nodes.map((n) => ({
5271
+ id: n.id,
5272
+ graph_id: n.graph_id,
5273
+ label: n.label,
5274
+ type: n.type,
5275
+ visit_count: n.visit_count,
5276
+ metadata: n.metadata,
5277
+ subgraph_id: n.subgraph_id,
5278
+ created_at: n.created_at,
5279
+ last_seen: n.last_seen
5280
+ // Omit embeddings — too large, recomputed locally
5281
+ })), null, 2);
5282
+ const edges = this.adapter.getAllEdges();
5283
+ const edgesJson = JSON.stringify(edges.map((e) => ({
5284
+ id: e.id,
5285
+ graph_id: e.graph_id,
5286
+ from_node: e.from_node,
5287
+ to_node: e.to_node,
5288
+ type: e.type,
5289
+ weight: e.weight,
5290
+ locked: e.locked,
5291
+ decay_rate: e.decay_rate,
5292
+ traversal_count: e.traversal_count,
5293
+ created_at: e.created_at
5294
+ })), null, 2);
5295
+ const weightEvents = this.getRecentWeightEvents(500);
5296
+ const weightEventsJsonl = weightEvents.map((e) => JSON.stringify(e)).join("\n");
5297
+ const personalityMap = this.exportPersonalityProfiles();
5298
+ const conversations = this.getConversations(1e3);
5299
+ const convsJsonl = conversations.map((c) => JSON.stringify(c)).join("\n");
5300
+ const { token, owner, repo } = this.config;
5301
+ const pushes = [
5302
+ putFile(token, owner, repo, "graph/nodes.json", nodesJson, commitMsg),
5303
+ putFile(token, owner, repo, "graph/edges.json", edgesJson, commitMsg)
5304
+ ];
5305
+ if (weightEventsJsonl) {
5306
+ pushes.push(putFile(token, owner, repo, "graph/weight_events.jsonl", weightEventsJsonl, commitMsg));
5307
+ }
5308
+ if (convsJsonl) {
5309
+ pushes.push(putFile(token, owner, repo, "memory/conversations.jsonl", convsJsonl, commitMsg));
5310
+ }
5311
+ for (const [entityId, profile] of Object.entries(personalityMap)) {
5312
+ pushes.push(
5313
+ putFile(
5314
+ token,
5315
+ owner,
5316
+ repo,
5317
+ `memory/personalities/${entityId}.json`,
5318
+ JSON.stringify(profile, null, 2),
5319
+ commitMsg
5320
+ )
5321
+ );
5322
+ }
5323
+ const customSkillsDir = resolve8(homedir5(), ".0agent", "skills", "custom");
5324
+ if (existsSync9(customSkillsDir)) {
5325
+ for (const file of readdirSync4(customSkillsDir).filter((f) => f.endsWith(".yaml"))) {
5326
+ const content = readFileSync9(resolve8(customSkillsDir, file), "utf8");
5327
+ pushes.push(putFile(token, owner, repo, `skills/custom/${file}`, content, commitMsg));
5328
+ }
5329
+ }
5330
+ const readme = this.generateReadme(nodes.length, edges.length);
5331
+ pushes.push(putFile(token, owner, repo, "README.md", readme, commitMsg));
5332
+ await Promise.all(pushes);
5333
+ this.lastPushAt = now;
5334
+ this.pendingChanges = false;
5335
+ return { pushed: true, pulled: false, nodes_synced: nodes.length, edges_synced: edges.length, timestamp: now };
5336
+ } catch (err) {
5337
+ return {
5338
+ pushed: false,
5339
+ pulled: false,
5340
+ nodes_synced: 0,
5341
+ edges_synced: 0,
5342
+ error: err instanceof Error ? err.message : String(err),
5343
+ timestamp: now
5344
+ };
5345
+ }
5346
+ }
5347
+ /**
5348
+ * Pull graph state from GitHub and merge into local SQLite.
5349
+ * Safe: never overwrites locked edges or newer local data.
5350
+ */
5351
+ async pull() {
5352
+ const now = Date.now();
5353
+ try {
5354
+ const { token, owner, repo } = this.config;
5355
+ const [nodesJson, edgesJson] = await Promise.all([
5356
+ getFile(token, owner, repo, "graph/nodes.json"),
5357
+ getFile(token, owner, repo, "graph/edges.json")
5358
+ ]);
5359
+ let nodeCount = 0, edgeCount = 0;
5360
+ if (nodesJson) {
5361
+ const nodes = JSON.parse(nodesJson);
5362
+ for (const n of nodes) {
5363
+ const existing = this.graph.getNode(String(n.id));
5364
+ if (!existing) {
5365
+ this.graph.addNode({
5366
+ id: String(n.id),
5367
+ graph_id: String(n.graph_id ?? "root"),
5368
+ label: String(n.label),
5369
+ type: n.type,
5370
+ created_at: Number(n.created_at ?? now),
5371
+ last_seen: Number(n.last_seen ?? now),
5372
+ visit_count: Number(n.visit_count ?? 1),
5373
+ metadata: n.metadata ?? {},
5374
+ subgraph_id: n.subgraph_id ? String(n.subgraph_id) : null,
5375
+ embedding: null,
5376
+ embedding_model: null,
5377
+ embedding_at: null,
5378
+ content: []
5379
+ });
5380
+ nodeCount++;
5381
+ }
5382
+ }
5383
+ }
5384
+ if (edgesJson) {
5385
+ const edges = JSON.parse(edgesJson);
5386
+ for (const e of edges) {
5387
+ const existing = this.graph.getEdge(String(e.id));
5388
+ if (!existing) {
5389
+ this.graph.addEdge({
5390
+ id: String(e.id),
5391
+ graph_id: String(e.graph_id ?? "root"),
5392
+ from_node: String(e.from_node),
5393
+ to_node: String(e.to_node),
5394
+ type: e.type,
5395
+ weight: Number(e.weight ?? 0.5),
5396
+ locked: Boolean(e.locked),
5397
+ decay_rate: Number(e.decay_rate ?? 1e-3),
5398
+ created_at: Number(e.created_at ?? now),
5399
+ last_traversed: null,
5400
+ traversal_count: Number(e.traversal_count ?? 0),
5401
+ metadata: {}
5402
+ });
5403
+ edgeCount++;
5404
+ } else if (!existing.locked) {
5405
+ const remoteWeight = Number(e.weight ?? 0.5);
5406
+ const blended = existing.weight * 0.6 + remoteWeight * 0.4;
5407
+ if (Math.abs(blended - existing.weight) > 0.01) {
5408
+ this.adapter.forceUpdateEdgeWeight(existing.id, blended);
5409
+ edgeCount++;
5410
+ }
5411
+ }
5412
+ }
5413
+ }
5414
+ await this.pullCustomSkills();
5415
+ this.lastPullAt = now;
5416
+ return { pushed: false, pulled: true, nodes_synced: nodeCount, edges_synced: edgeCount, timestamp: now };
5417
+ } catch (err) {
5418
+ return {
5419
+ pushed: false,
5420
+ pulled: false,
5421
+ nodes_synced: 0,
5422
+ edges_synced: 0,
5423
+ error: err instanceof Error ? err.message : String(err),
5424
+ timestamp: now
5425
+ };
5426
+ }
5427
+ }
5428
+ /** Mark that changes exist — scheduler will push on next interval. */
5429
+ markDirty() {
5430
+ this.pendingChanges = true;
5431
+ }
5432
+ hasPendingChanges() {
5433
+ return this.pendingChanges;
5434
+ }
5435
+ getLastSyncTimes() {
5436
+ return { pushed_at: this.lastPushAt, pulled_at: this.lastPullAt };
5437
+ }
5438
+ // ─── Repo creation ─────────────────────────────────────────────────────────
5439
+ /**
5440
+ * Create the private GitHub repo for memory storage.
5441
+ * Returns the repo URL on success.
5442
+ */
5443
+ static async createRepo(token, owner, repoName) {
5444
+ const res = await ghFetch("/user/repos", token, {
5445
+ method: "POST",
5446
+ body: JSON.stringify({
5447
+ name: repoName,
5448
+ description: "0agent memory \u2014 knowledge graph, personality profiles, conversation history",
5449
+ private: true,
5450
+ auto_init: true
5451
+ // creates main branch with README
5452
+ })
5453
+ });
5454
+ if (!res.ok) {
5455
+ const err = await res.json();
5456
+ if (res.status !== 422) throw new Error(`Failed to create repo: ${err.message ?? res.status}`);
5457
+ }
5458
+ return `https://github.com/${owner}/${repoName}`;
5459
+ }
5460
+ /**
5461
+ * Verify token works and return the authenticated username.
5462
+ */
5463
+ static async verifyToken(token) {
5464
+ try {
5465
+ const res = await ghFetch("/user", token);
5466
+ if (!res.ok) return null;
5467
+ const user = await res.json();
5468
+ return user.login;
5469
+ } catch {
5470
+ return null;
5471
+ }
5472
+ }
5473
+ // ─── Private helpers ───────────────────────────────────────────────────────
5474
+ getRecentWeightEvents(limit) {
5475
+ try {
5476
+ const db = this.adapter.db;
5477
+ return db.prepare(`SELECT * FROM weight_events ORDER BY created_at DESC LIMIT ?`).all(limit);
5478
+ } catch {
5479
+ return [];
5480
+ }
5481
+ }
5482
+ getConversations(limit) {
5483
+ try {
5484
+ const db = this.adapter.db;
5485
+ return db.prepare(`SELECT * FROM conversations ORDER BY created_at DESC LIMIT ?`).all(limit);
5486
+ } catch {
5487
+ return [];
5488
+ }
5489
+ }
5490
+ exportPersonalityProfiles() {
5491
+ const profiles = {};
5492
+ try {
5493
+ const profileNodes = this.adapter.queryNodes({ type: "signal" }).filter(
5494
+ (n) => n.metadata?.is_personality_profile
5495
+ );
5496
+ for (const node of profileNodes) {
5497
+ const entityId = node.metadata?.entity_id;
5498
+ if (entityId && node.content?.[0]?.data) {
5499
+ try {
5500
+ profiles[entityId] = JSON.parse(node.content[0].data);
5501
+ } catch {
5502
+ }
5503
+ }
5504
+ }
5505
+ } catch {
5506
+ }
5507
+ return profiles;
5508
+ }
5509
+ async pullCustomSkills() {
5510
+ const { token, owner, repo } = this.config;
5511
+ const dir = resolve8(homedir5(), ".0agent", "skills", "custom");
5512
+ try {
5513
+ const res = await ghFetch(`/repos/${owner}/${repo}/contents/skills/custom`, token);
5514
+ if (!res.ok) return;
5515
+ const files = await res.json();
5516
+ for (const file of files.filter((f) => f.name.endsWith(".yaml"))) {
5517
+ const content = await getFile(token, owner, repo, `skills/custom/${file.name}`);
5518
+ if (content) {
5519
+ const { mkdirSync: mkdirSync7 } = await import("node:fs");
5520
+ mkdirSync7(dir, { recursive: true });
5521
+ writeFileSync6(resolve8(dir, file.name), content, "utf8");
5522
+ }
5523
+ }
5524
+ } catch {
5525
+ }
5526
+ }
5527
+ generateReadme(nodeCount, edgeCount) {
5528
+ return `# 0agent Memory
5529
+
5530
+ > Private knowledge graph \u2014 backed up automatically by [0agent](https://github.com/cadetmaze/0agentv1)
5531
+
5532
+ **Last synced:** ${(/* @__PURE__ */ new Date()).toISOString()}
5533
+
5534
+ ## Contents
5535
+
5536
+ | File | Description |
5537
+ |------|-------------|
5538
+ | \`graph/nodes.json\` | ${nodeCount} entities, strategies, plans, context nodes |
5539
+ | \`graph/edges.json\` | ${edgeCount} relationships with learned weights |
5540
+ | \`graph/weight_events.jsonl\` | Full learning history (append-only) |
5541
+ | \`memory/personalities/\` | Communication style + preferences per person |
5542
+ | \`memory/conversations.jsonl\` | Conversation history |
5543
+ | \`skills/custom/\` | Custom skill definitions |
5544
+
5545
+ ## Cross-device sync
5546
+
5547
+ \`\`\`bash
5548
+ # On a new machine:
5549
+ 0agent memory connect github --repo ${this.config.owner}/${this.config.repo}
5550
+ \`\`\`
5551
+
5552
+ ## Rollback
5553
+
5554
+ \`\`\`bash
5555
+ # Restore to a previous state:
5556
+ git log --oneline # find the commit
5557
+ git checkout <commit> graph/ # restore graph files
5558
+ 0agent memory import # import into local SQLite
5559
+ \`\`\`
5560
+
5561
+ ---
5562
+ *This repo is managed by 0agent. Do not edit files manually.*
5563
+ `;
5564
+ }
5565
+ };
5566
+
5151
5567
  // packages/daemon/src/ZeroAgentDaemon.ts
5152
5568
  var ZeroAgentDaemon = class {
5153
5569
  config = null;
@@ -5160,15 +5576,18 @@ var ZeroAgentDaemon = class {
5160
5576
  httpServer = null;
5161
5577
  skillRegistry = null;
5162
5578
  backgroundWorkers = null;
5579
+ githubMemorySync = null;
5580
+ memorySyncTimer = null;
5581
+ proactiveSurfaceInstance = null;
5163
5582
  startedAt = 0;
5164
5583
  pidFilePath;
5165
5584
  constructor() {
5166
- this.pidFilePath = resolve9(homedir5(), ".0agent", "daemon.pid");
5585
+ this.pidFilePath = resolve10(homedir6(), ".0agent", "daemon.pid");
5167
5586
  }
5168
5587
  async start(opts) {
5169
5588
  this.config = await loadConfig(opts?.config_path);
5170
- const dotDir = resolve9(homedir5(), ".0agent");
5171
- if (!existsSync10(dotDir)) {
5589
+ const dotDir = resolve10(homedir6(), ".0agent");
5590
+ if (!existsSync11(dotDir)) {
5172
5591
  mkdirSync6(dotDir, { recursive: true });
5173
5592
  }
5174
5593
  this.adapter = new SQLiteAdapter({ db_path: this.config.graph.db_path });
@@ -5192,6 +5611,19 @@ var ZeroAgentDaemon = class {
5192
5611
  } else {
5193
5612
  console.warn("[0agent] No LLM API key configured \u2014 tasks will not call the LLM");
5194
5613
  }
5614
+ const ghMemCfg = this.config["github_memory"];
5615
+ if (ghMemCfg?.enabled && ghMemCfg.token && ghMemCfg.owner && ghMemCfg.repo) {
5616
+ this.githubMemorySync = new GitHubMemorySync(
5617
+ { token: ghMemCfg.token, owner: ghMemCfg.owner, repo: ghMemCfg.repo },
5618
+ this.adapter,
5619
+ this.graph
5620
+ );
5621
+ console.log(`[0agent] Memory sync: github.com/${ghMemCfg.owner}/${ghMemCfg.repo}`);
5622
+ this.githubMemorySync.pull().then((r) => {
5623
+ if (r.pulled) console.log(`[0agent] Memory pulled: +${r.nodes_synced} nodes, +${r.edges_synced} edges`);
5624
+ }).catch(() => {
5625
+ });
5626
+ }
5195
5627
  const cwd = process.env["ZEROAGENT_CWD"] ?? process.cwd();
5196
5628
  const identityManager = new IdentityManager(this.graph);
5197
5629
  const identity = await identityManager.init().catch(() => null);
@@ -5221,6 +5653,18 @@ var ZeroAgentDaemon = class {
5221
5653
  // enables ConversationStore + weight feedback
5222
5654
  });
5223
5655
  const teamSync = identity && teams.length > 0 ? new TeamSync(teamManager, this.adapter, identity.entity_node_id) : null;
5656
+ if (this.githubMemorySync) {
5657
+ const memSync = this.githubMemorySync;
5658
+ this.memorySyncTimer = setInterval(async () => {
5659
+ if (memSync.hasPendingChanges()) {
5660
+ const result = await memSync.push().catch(() => null);
5661
+ if (result?.pushed) {
5662
+ console.log(`[0agent] Memory auto-synced: ${result.nodes_synced} nodes`);
5663
+ }
5664
+ }
5665
+ }, 30 * 60 * 1e3);
5666
+ if (typeof this.memorySyncTimer === "object") this.memorySyncTimer.unref?.();
5667
+ }
5224
5668
  let proactiveSurface = null;
5225
5669
  try {
5226
5670
  const { ProactiveSurface: ProactiveSurface2 } = await Promise.resolve().then(() => (init_ProactiveSurface(), ProactiveSurface_exports));
@@ -5239,6 +5683,7 @@ var ZeroAgentDaemon = class {
5239
5683
  active_sessions: this.sessionManager.activeSessionCount()
5240
5684
  }));
5241
5685
  this.startedAt = Date.now();
5686
+ const memSyncRef = this.githubMemorySync;
5242
5687
  this.httpServer = new HTTPServer({
5243
5688
  port: this.config.server.port,
5244
5689
  host: this.config.server.host,
@@ -5246,10 +5691,12 @@ var ZeroAgentDaemon = class {
5246
5691
  graph: this.graph,
5247
5692
  traceStore: this.traceStore,
5248
5693
  skillRegistry: this.skillRegistry,
5249
- getStatus: () => this.getStatus()
5694
+ getStatus: () => this.getStatus(),
5695
+ getMemorySync: () => memSyncRef,
5696
+ proactiveSurface
5250
5697
  });
5251
5698
  await this.httpServer.start();
5252
- writeFileSync6(this.pidFilePath, String(process.pid), "utf8");
5699
+ writeFileSync7(this.pidFilePath, String(process.pid), "utf8");
5253
5700
  console.log(
5254
5701
  `[0agent] Daemon started on ${this.config.server.host}:${this.config.server.port} (PID: ${process.pid})`
5255
5702
  );
@@ -5274,6 +5721,15 @@ var ZeroAgentDaemon = class {
5274
5721
  this.backgroundWorkers.stop();
5275
5722
  this.backgroundWorkers = null;
5276
5723
  }
5724
+ if (this.githubMemorySync?.hasPendingChanges()) {
5725
+ await this.githubMemorySync.push("sync: daemon shutdown").catch(() => {
5726
+ });
5727
+ }
5728
+ if (this.memorySyncTimer) {
5729
+ clearInterval(this.memorySyncTimer);
5730
+ this.memorySyncTimer = null;
5731
+ }
5732
+ this.githubMemorySync = null;
5277
5733
  this.sessionManager = null;
5278
5734
  this.skillRegistry = null;
5279
5735
  this.inferenceEngine = null;
@@ -5283,7 +5739,7 @@ var ZeroAgentDaemon = class {
5283
5739
  this.graph = null;
5284
5740
  }
5285
5741
  this.adapter = null;
5286
- if (existsSync10(this.pidFilePath)) {
5742
+ if (existsSync11(this.pidFilePath)) {
5287
5743
  try {
5288
5744
  unlinkSync2(this.pidFilePath);
5289
5745
  } catch {
@@ -5313,11 +5769,11 @@ var ZeroAgentDaemon = class {
5313
5769
  };
5314
5770
 
5315
5771
  // packages/daemon/src/start.ts
5316
- import { resolve as resolve10 } from "node:path";
5317
- import { homedir as homedir6 } from "node:os";
5318
- import { existsSync as existsSync11 } from "node:fs";
5319
- var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve10(homedir6(), ".0agent", "config.yaml");
5320
- if (!existsSync11(CONFIG_PATH)) {
5772
+ import { resolve as resolve11 } from "node:path";
5773
+ import { homedir as homedir7 } from "node:os";
5774
+ import { existsSync as existsSync12 } from "node:fs";
5775
+ var CONFIG_PATH = process.env["ZEROAGENT_CONFIG"] ?? resolve11(homedir7(), ".0agent", "config.yaml");
5776
+ if (!existsSync12(CONFIG_PATH)) {
5321
5777
  console.error(`
5322
5778
  0agent is not initialised.
5323
5779
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "A persistent, learning AI agent that runs on your machine. An agent that learns.",
5
5
  "private": false,
6
6
  "license": "Apache-2.0",
@@ -24,6 +24,7 @@
24
24
  "dependencies": {
25
25
  "@hono/node-server": "^1.13.0",
26
26
  "better-sqlite3": "^11.6.0",
27
+ "enquirer": "^2.4.1",
27
28
  "hono": "^4.6.0",
28
29
  "ws": "^8.18.0",
29
30
  "yaml": "^2.6.0",