@vsuryav/agent-sim 0.1.0
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/README.md +25 -0
- package/bin/agent-sim.js +25 -0
- package/package.json +72 -0
- package/src/app-paths.ts +29 -0
- package/src/app-sync.test.ts +75 -0
- package/src/app-sync.ts +110 -0
- package/src/cli.ts +129 -0
- package/src/collector/claude-code.test.ts +102 -0
- package/src/collector/claude-code.ts +133 -0
- package/src/collector/codex-cli.test.ts +116 -0
- package/src/collector/codex-cli.ts +149 -0
- package/src/collector/db.test.ts +59 -0
- package/src/collector/db.ts +125 -0
- package/src/collector/names.test.ts +21 -0
- package/src/collector/names.ts +28 -0
- package/src/collector/personality.test.ts +40 -0
- package/src/collector/personality.ts +46 -0
- package/src/collector/remote-sync.test.ts +31 -0
- package/src/collector/remote-sync.ts +171 -0
- package/src/collector/sync.test.ts +67 -0
- package/src/collector/sync.ts +148 -0
- package/src/collector/types.ts +1 -0
- package/src/engine/bootstrap/state.ts +3 -0
- package/src/engine/buddy/CompanionSprite.tsx +371 -0
- package/src/engine/buddy/companion.ts +133 -0
- package/src/engine/buddy/prompt.ts +36 -0
- package/src/engine/buddy/sprites.ts +514 -0
- package/src/engine/buddy/types.ts +148 -0
- package/src/engine/buddy/useBuddyNotification.tsx +98 -0
- package/src/engine/ink/Ansi.tsx +292 -0
- package/src/engine/ink/bidi.ts +139 -0
- package/src/engine/ink/clearTerminal.ts +74 -0
- package/src/engine/ink/colorize.ts +231 -0
- package/src/engine/ink/components/AlternateScreen.tsx +80 -0
- package/src/engine/ink/components/App.tsx +658 -0
- package/src/engine/ink/components/AppContext.ts +21 -0
- package/src/engine/ink/components/Box.tsx +214 -0
- package/src/engine/ink/components/Button.tsx +192 -0
- package/src/engine/ink/components/ClockContext.tsx +112 -0
- package/src/engine/ink/components/CursorDeclarationContext.ts +32 -0
- package/src/engine/ink/components/ErrorOverview.tsx +109 -0
- package/src/engine/ink/components/Link.tsx +42 -0
- package/src/engine/ink/components/Newline.tsx +39 -0
- package/src/engine/ink/components/NoSelect.tsx +68 -0
- package/src/engine/ink/components/RawAnsi.tsx +57 -0
- package/src/engine/ink/components/ScrollBox.tsx +237 -0
- package/src/engine/ink/components/Spacer.tsx +20 -0
- package/src/engine/ink/components/StdinContext.ts +49 -0
- package/src/engine/ink/components/TerminalFocusContext.tsx +52 -0
- package/src/engine/ink/components/TerminalSizeContext.tsx +7 -0
- package/src/engine/ink/components/Text.tsx +254 -0
- package/src/engine/ink/constants.ts +2 -0
- package/src/engine/ink/dom.ts +484 -0
- package/src/engine/ink/events/click-event.ts +38 -0
- package/src/engine/ink/events/dispatcher.ts +233 -0
- package/src/engine/ink/events/emitter.ts +39 -0
- package/src/engine/ink/events/event-handlers.ts +73 -0
- package/src/engine/ink/events/event.ts +11 -0
- package/src/engine/ink/events/focus-event.ts +21 -0
- package/src/engine/ink/events/input-event.ts +205 -0
- package/src/engine/ink/events/keyboard-event.ts +51 -0
- package/src/engine/ink/events/terminal-event.ts +107 -0
- package/src/engine/ink/events/terminal-focus-event.ts +19 -0
- package/src/engine/ink/focus.ts +181 -0
- package/src/engine/ink/frame.ts +124 -0
- package/src/engine/ink/get-max-width.ts +27 -0
- package/src/engine/ink/global.d.ts +18 -0
- package/src/engine/ink/hit-test.ts +130 -0
- package/src/engine/ink/hooks/use-animation-frame.ts +57 -0
- package/src/engine/ink/hooks/use-app.ts +8 -0
- package/src/engine/ink/hooks/use-declared-cursor.ts +73 -0
- package/src/engine/ink/hooks/use-input.ts +92 -0
- package/src/engine/ink/hooks/use-interval.ts +67 -0
- package/src/engine/ink/hooks/use-search-highlight.ts +53 -0
- package/src/engine/ink/hooks/use-selection.ts +104 -0
- package/src/engine/ink/hooks/use-stdin.ts +8 -0
- package/src/engine/ink/hooks/use-tab-status.ts +72 -0
- package/src/engine/ink/hooks/use-terminal-focus.ts +16 -0
- package/src/engine/ink/hooks/use-terminal-title.ts +31 -0
- package/src/engine/ink/hooks/use-terminal-viewport.ts +96 -0
- package/src/engine/ink/ink.tsx +1723 -0
- package/src/engine/ink/instances.ts +10 -0
- package/src/engine/ink/layout/engine.ts +6 -0
- package/src/engine/ink/layout/geometry.ts +97 -0
- package/src/engine/ink/layout/node.ts +152 -0
- package/src/engine/ink/layout/yoga.ts +308 -0
- package/src/engine/ink/line-width-cache.ts +24 -0
- package/src/engine/ink/log-update.ts +773 -0
- package/src/engine/ink/measure-element.ts +23 -0
- package/src/engine/ink/measure-text.ts +47 -0
- package/src/engine/ink/node-cache.ts +54 -0
- package/src/engine/ink/optimizer.ts +93 -0
- package/src/engine/ink/output.ts +797 -0
- package/src/engine/ink/parse-keypress.ts +801 -0
- package/src/engine/ink/reconciler.ts +512 -0
- package/src/engine/ink/render-border.ts +231 -0
- package/src/engine/ink/render-node-to-output.ts +1462 -0
- package/src/engine/ink/render-to-screen.ts +231 -0
- package/src/engine/ink/renderer.ts +178 -0
- package/src/engine/ink/root.ts +184 -0
- package/src/engine/ink/screen.ts +1486 -0
- package/src/engine/ink/searchHighlight.ts +93 -0
- package/src/engine/ink/selection.ts +917 -0
- package/src/engine/ink/squash-text-nodes.ts +92 -0
- package/src/engine/ink/stringWidth.ts +222 -0
- package/src/engine/ink/styles.ts +771 -0
- package/src/engine/ink/supports-hyperlinks.ts +57 -0
- package/src/engine/ink/tabstops.ts +46 -0
- package/src/engine/ink/terminal-focus-state.ts +47 -0
- package/src/engine/ink/terminal-querier.ts +212 -0
- package/src/engine/ink/terminal.ts +248 -0
- package/src/engine/ink/termio/ansi.ts +75 -0
- package/src/engine/ink/termio/csi.ts +319 -0
- package/src/engine/ink/termio/dec.ts +60 -0
- package/src/engine/ink/termio/esc.ts +67 -0
- package/src/engine/ink/termio/osc.ts +493 -0
- package/src/engine/ink/termio/parser.ts +394 -0
- package/src/engine/ink/termio/sgr.ts +308 -0
- package/src/engine/ink/termio/tokenize.ts +319 -0
- package/src/engine/ink/termio/types.ts +236 -0
- package/src/engine/ink/useTerminalNotification.ts +126 -0
- package/src/engine/ink/warn.ts +9 -0
- package/src/engine/ink/widest-line.ts +19 -0
- package/src/engine/ink/wrap-text.ts +74 -0
- package/src/engine/ink/wrapAnsi.ts +20 -0
- package/src/engine/native-ts/yoga-layout/enums.ts +134 -0
- package/src/engine/native-ts/yoga-layout/index.ts +2578 -0
- package/src/engine/stubs/bootstrap-state.ts +4 -0
- package/src/engine/stubs/debug.ts +6 -0
- package/src/engine/stubs/log.ts +4 -0
- package/src/engine/utils/debug.ts +5 -0
- package/src/engine/utils/earlyInput.ts +4 -0
- package/src/engine/utils/env.ts +15 -0
- package/src/engine/utils/envUtils.ts +4 -0
- package/src/engine/utils/execFileNoThrow.ts +24 -0
- package/src/engine/utils/fullscreen.ts +4 -0
- package/src/engine/utils/intl.ts +9 -0
- package/src/engine/utils/log.ts +3 -0
- package/src/engine/utils/semver.ts +13 -0
- package/src/engine/utils/sliceAnsi.ts +10 -0
- package/src/engine/utils/theme.ts +17 -0
- package/src/game/App.tsx +141 -0
- package/src/game/agents/behavior.ts +249 -0
- package/src/game/agents/speech.ts +57 -0
- package/src/game/canvas.ts +98 -0
- package/src/game/launch.ts +36 -0
- package/src/game/ship/ShipView.tsx +145 -0
- package/src/game/ship/ship-map.ts +172 -0
- package/src/game/ui/AgentBio.tsx +72 -0
- package/src/game/ui/HUD.tsx +63 -0
- package/src/game/ui/StatusBar.tsx +49 -0
- package/src/game/useKeyboard.ts +62 -0
- package/src/main.tsx +22 -0
- package/src/run-interactive.ts +74 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface Traits {
|
|
2
|
+
builder: number;
|
|
3
|
+
creator: number;
|
|
4
|
+
explorer: number;
|
|
5
|
+
leader: number;
|
|
6
|
+
thinker: number;
|
|
7
|
+
scholar: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function extractTraits(
|
|
11
|
+
toolCounts: Record<string, number>,
|
|
12
|
+
contentTypeCounts: Record<string, number>,
|
|
13
|
+
messageCount: number,
|
|
14
|
+
durationMs: number,
|
|
15
|
+
): Traits {
|
|
16
|
+
const totalToolUse = Object.values(toolCounts).reduce((a, b) => a + b, 0) || 1;
|
|
17
|
+
const totalContent = Object.values(contentTypeCounts).reduce((a, b) => a + b, 0) || 1;
|
|
18
|
+
|
|
19
|
+
// Builder: Bash, shell commands
|
|
20
|
+
const bashCount = (toolCounts['Bash'] ?? 0) + (toolCounts['bash'] ?? 0);
|
|
21
|
+
const builder = Math.min(1, bashCount / totalToolUse * 2);
|
|
22
|
+
|
|
23
|
+
// Creator: Write, Edit, NotebookEdit
|
|
24
|
+
const writeCount = (toolCounts['Write'] ?? 0) + (toolCounts['Edit'] ?? 0) + (toolCounts['NotebookEdit'] ?? 0);
|
|
25
|
+
const creator = Math.min(1, writeCount / totalToolUse * 2.5);
|
|
26
|
+
|
|
27
|
+
// Explorer: Read, Grep, Glob, LS
|
|
28
|
+
const readCount = (toolCounts['Read'] ?? 0) + (toolCounts['Grep'] ?? 0)
|
|
29
|
+
+ (toolCounts['Glob'] ?? 0) + (toolCounts['LS'] ?? 0);
|
|
30
|
+
const explorer = Math.min(1, readCount / totalToolUse * 2);
|
|
31
|
+
|
|
32
|
+
// Leader: Agent, subagent delegation
|
|
33
|
+
const agentCount = toolCounts['Agent'] ?? 0;
|
|
34
|
+
const leader = Math.min(1, agentCount / Math.max(1, totalToolUse) * 8);
|
|
35
|
+
|
|
36
|
+
// Thinker: thinking blocks relative to total content
|
|
37
|
+
const thinkingCount = contentTypeCounts['thinking'] ?? 0;
|
|
38
|
+
const thinker = Math.min(1, thinkingCount / totalContent * 3);
|
|
39
|
+
|
|
40
|
+
// Scholar: WebSearch, WebFetch
|
|
41
|
+
const searchCount = (toolCounts['WebSearch'] ?? 0) + (toolCounts['WebFetch'] ?? 0)
|
|
42
|
+
+ (toolCounts['web_search_end'] ?? 0);
|
|
43
|
+
const scholar = Math.min(1, searchCount / Math.max(1, totalToolUse) * 5);
|
|
44
|
+
|
|
45
|
+
return { builder, creator, explorer, leader, thinker, scholar };
|
|
46
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { buildPushPayload, getDeviceId } from './remote-sync.js';
|
|
3
|
+
|
|
4
|
+
describe('remote-sync', () => {
|
|
5
|
+
it('getDeviceId returns consistent hash', () => {
|
|
6
|
+
const id1 = getDeviceId();
|
|
7
|
+
const id2 = getDeviceId();
|
|
8
|
+
expect(id1).toBe(id2);
|
|
9
|
+
expect(typeof id1).toBe('string');
|
|
10
|
+
expect(id1.length).toBeGreaterThan(10);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('buildPushPayload formats agents correctly', () => {
|
|
14
|
+
const agents = [
|
|
15
|
+
{
|
|
16
|
+
id: 'test-1', tool: 'claude-code', model: 'opus', project: '/p', clan: 'p',
|
|
17
|
+
created_at: 1000, total_tokens: 500, input_tokens: 100, output_tokens: 400,
|
|
18
|
+
cache_tokens: 0, reasoning_tokens: 0, duration_ms: 0, message_count: 5,
|
|
19
|
+
trait_builder: 0.5, trait_creator: 0.3, trait_explorer: 0.2,
|
|
20
|
+
trait_leader: 0.0, trait_thinker: 0.1, trait_scholar: 0.0,
|
|
21
|
+
name: 'Bold Spark', maturity: 'spark',
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const payload = buildPushPayload(agents);
|
|
26
|
+
expect(payload.device_id).toBeTruthy();
|
|
27
|
+
expect(payload.hostname).toBeTruthy();
|
|
28
|
+
expect(payload.agents).toHaveLength(1);
|
|
29
|
+
expect(payload.agents[0]!.id).toBe('test-1');
|
|
30
|
+
});
|
|
31
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import {
|
|
6
|
+
authLoginStartResponseSchema,
|
|
7
|
+
authSessionResponseSchema,
|
|
8
|
+
pullResponseSchema,
|
|
9
|
+
type CollectedAgent,
|
|
10
|
+
type PullResponse,
|
|
11
|
+
} from '@vsuryav/agent-sim-contracts';
|
|
12
|
+
import { APP_NAME, CONFIG_PATH, ensureLocalDataDir } from '../app-paths.js';
|
|
13
|
+
|
|
14
|
+
const LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
|
|
15
|
+
export const DEFAULT_SERVER_URL = process.env['AGENT_SIM_SERVER_URL']
|
|
16
|
+
?? process.env['AGENT_WARS_SERVER_URL']
|
|
17
|
+
?? 'http://localhost:3456';
|
|
18
|
+
|
|
19
|
+
interface Config {
|
|
20
|
+
serverUrl?: string;
|
|
21
|
+
token?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function loadConfig(): Config {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')) as Config;
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveConfig(config: Config): void {
|
|
33
|
+
ensureLocalDataDir();
|
|
34
|
+
fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
|
|
35
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function hasRemoteConfig(): boolean {
|
|
39
|
+
const config = loadConfig();
|
|
40
|
+
return !!config.serverUrl && !!config.token;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function getDeviceId(): string {
|
|
44
|
+
const raw = `${os.hostname()}-${os.userInfo().username}-${os.platform()}-${os.arch()}`;
|
|
45
|
+
return createHash('sha256').update(raw).digest('hex').slice(0, 32);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function buildPushPayload(agents: CollectedAgent[]): { device_id: string; hostname: string; agents: CollectedAgent[] } {
|
|
49
|
+
return {
|
|
50
|
+
device_id: getDeviceId(),
|
|
51
|
+
hostname: os.hostname(),
|
|
52
|
+
agents,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function sleep(ms: number): Promise<void> {
|
|
57
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function pollForLoginSession(
|
|
61
|
+
serverUrl: string,
|
|
62
|
+
sessionId: string,
|
|
63
|
+
pollIntervalMs: number,
|
|
64
|
+
): Promise<{ userId: number; githubLogin: string; token: string }> {
|
|
65
|
+
const deadline = Date.now() + LOGIN_TIMEOUT_MS;
|
|
66
|
+
|
|
67
|
+
while (Date.now() < deadline) {
|
|
68
|
+
const res = await fetch(`${serverUrl}/auth/session/${sessionId}`);
|
|
69
|
+
if (res.status === 202) {
|
|
70
|
+
await sleep(pollIntervalMs);
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const body = await res.json() as unknown;
|
|
75
|
+
if (!res.ok) {
|
|
76
|
+
const error = body as { error?: string };
|
|
77
|
+
throw new Error(`Login failed: ${error.error ?? res.statusText}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const session = authSessionResponseSchema.parse(body);
|
|
81
|
+
if (session.status === 'complete') {
|
|
82
|
+
return session;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
await sleep(pollIntervalMs);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
throw new Error(`Login timed out. Re-run the ${APP_NAME} login command and finish GitHub auth within 5 minutes.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function login(serverUrl: string): Promise<void> {
|
|
92
|
+
const config = loadConfig();
|
|
93
|
+
config.serverUrl = serverUrl;
|
|
94
|
+
saveConfig(config);
|
|
95
|
+
|
|
96
|
+
const res = await fetch(`${serverUrl}/auth/login`);
|
|
97
|
+
const body = await res.json() as unknown;
|
|
98
|
+
if (!res.ok) {
|
|
99
|
+
const error = body as { error?: string };
|
|
100
|
+
throw new Error(`Login failed: ${error.error ?? res.statusText}`);
|
|
101
|
+
}
|
|
102
|
+
const { url, session_id, poll_interval_ms } = authLoginStartResponseSchema.parse(body);
|
|
103
|
+
|
|
104
|
+
console.log(`\n Open this URL to login with GitHub:\n`);
|
|
105
|
+
console.log(` ${url}\n`);
|
|
106
|
+
console.log(' Waiting for GitHub authorization to complete...\n');
|
|
107
|
+
|
|
108
|
+
const session = await pollForLoginSession(serverUrl, session_id, poll_interval_ms);
|
|
109
|
+
config.token = session.token;
|
|
110
|
+
saveConfig(config);
|
|
111
|
+
console.log(` Logged in as ${session.githubLogin}. Token saved.`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function setToken(token: string): void {
|
|
115
|
+
const config = loadConfig();
|
|
116
|
+
config.token = token;
|
|
117
|
+
saveConfig(config);
|
|
118
|
+
console.log(' Token saved.');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function getPreferredServerUrl(): string {
|
|
122
|
+
return loadConfig().serverUrl ?? DEFAULT_SERVER_URL;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function getRequiredRemoteConfig(): Required<Config> {
|
|
126
|
+
const config = loadConfig();
|
|
127
|
+
if (!config.serverUrl || !config.token) {
|
|
128
|
+
throw new Error(`Not logged in. Run: ${APP_NAME} login <server-url>`);
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
serverUrl: config.serverUrl,
|
|
132
|
+
token: config.token,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function pullFromServer(): Promise<PullResponse> {
|
|
137
|
+
const config = getRequiredRemoteConfig();
|
|
138
|
+
const res = await fetch(`${config.serverUrl}/sync/pull`, {
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Bearer ${config.token}`,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
if (!res.ok) {
|
|
145
|
+
const err = await res.json() as { error?: string };
|
|
146
|
+
throw new Error(`Pull failed: ${err.error ?? res.statusText}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const body = await res.json() as unknown;
|
|
150
|
+
return pullResponseSchema.parse(body);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function pushToServer(agents: CollectedAgent[]): Promise<{ synced: number }> {
|
|
154
|
+
const config = getRequiredRemoteConfig();
|
|
155
|
+
const payload = buildPushPayload(agents);
|
|
156
|
+
const res = await fetch(`${config.serverUrl}/sync/push`, {
|
|
157
|
+
method: 'POST',
|
|
158
|
+
headers: {
|
|
159
|
+
'Content-Type': 'application/json',
|
|
160
|
+
Authorization: `Bearer ${config.token}`,
|
|
161
|
+
},
|
|
162
|
+
body: JSON.stringify(payload),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
if (!res.ok) {
|
|
166
|
+
const err = await res.json() as { error?: string };
|
|
167
|
+
throw new Error(`Push failed: ${err.error ?? res.statusText}`);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return res.json() as Promise<{ synced: number }>;
|
|
171
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { syncAll } from './sync.js';
|
|
3
|
+
import { initDb, getDb, closeDb } from './db.js';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
|
|
8
|
+
describe('syncAll', () => {
|
|
9
|
+
const testDir = path.join(os.tmpdir(), `agent-sim-sync-test-${Date.now()}`);
|
|
10
|
+
const testDbPath = path.join(testDir, 'db.sqlite');
|
|
11
|
+
const mockClaudeDir = path.join(testDir, '.claude');
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
15
|
+
initDb(testDbPath);
|
|
16
|
+
fs.mkdirSync(path.join(mockClaudeDir, 'sessions'), { recursive: true });
|
|
17
|
+
fs.mkdirSync(path.join(mockClaudeDir, 'projects', '-test'), { recursive: true });
|
|
18
|
+
fs.writeFileSync(
|
|
19
|
+
path.join(mockClaudeDir, 'sessions', '1.json'),
|
|
20
|
+
JSON.stringify({ pid: 1, sessionId: 'sync-test-1', cwd: '/test', startedAt: 1700000000000 })
|
|
21
|
+
);
|
|
22
|
+
fs.writeFileSync(
|
|
23
|
+
path.join(mockClaudeDir, 'projects', '-test', 'sync-test-1.jsonl'),
|
|
24
|
+
JSON.stringify({
|
|
25
|
+
type: 'message',
|
|
26
|
+
message: {
|
|
27
|
+
role: 'assistant', model: 'claude-opus-4-6',
|
|
28
|
+
content: [{ type: 'tool_use', name: 'Bash', id: 't1', input: {} }],
|
|
29
|
+
usage: { input_tokens: 50, output_tokens: 100 },
|
|
30
|
+
},
|
|
31
|
+
uuid: 'm1', timestamp: 1700000001000, sessionId: 'sync-test-1',
|
|
32
|
+
})
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
closeDb();
|
|
38
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('inserts new agents into the database', () => {
|
|
42
|
+
const stats = syncAll({ claudeDir: mockClaudeDir, codexDir: path.join(testDir, '.codex-nonexistent') });
|
|
43
|
+
expect(stats.newAgents).toBe(1);
|
|
44
|
+
|
|
45
|
+
const db = getDb();
|
|
46
|
+
const agent = db.prepare('SELECT * FROM agents WHERE id = ?').get('sync-test-1') as any;
|
|
47
|
+
expect(agent).toBeTruthy();
|
|
48
|
+
expect(agent.name).toBeTruthy();
|
|
49
|
+
expect(agent.tool).toBe('claude-code');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('creates clan for new project', () => {
|
|
53
|
+
syncAll({ claudeDir: mockClaudeDir, codexDir: path.join(testDir, '.codex-nonexistent') });
|
|
54
|
+
|
|
55
|
+
const db = getDb();
|
|
56
|
+
const clans = db.prepare('SELECT * FROM clans').all();
|
|
57
|
+
expect(clans.length).toBeGreaterThan(0);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('computes maturity from token count', () => {
|
|
61
|
+
syncAll({ claudeDir: mockClaudeDir, codexDir: path.join(testDir, '.codex-nonexistent') });
|
|
62
|
+
|
|
63
|
+
const db = getDb();
|
|
64
|
+
const agent = db.prepare('SELECT maturity FROM agents WHERE id = ?').get('sync-test-1') as any;
|
|
65
|
+
expect(agent.maturity).toBe('spark');
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os from 'os';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { computeMaturity } from '@vsuryav/agent-sim-core';
|
|
4
|
+
import { getDb, setWorldConfig } from './db.js';
|
|
5
|
+
import { scanClaudeCode } from './claude-code.js';
|
|
6
|
+
import { scanCodexCli } from './codex-cli.js';
|
|
7
|
+
import { generateName } from './names.js';
|
|
8
|
+
import type { CollectedAgent, PullResponse } from './types.js';
|
|
9
|
+
|
|
10
|
+
const CLAN_COLORS = [
|
|
11
|
+
'#e94560', '#ffd166', '#52b788', '#a8dadc', '#e76f51',
|
|
12
|
+
'#6a4c93', '#1982c4', '#8ac926', '#ff595e', '#ffca3a',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export interface SyncOptions {
|
|
16
|
+
claudeDir?: string;
|
|
17
|
+
codexDir?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SyncResult {
|
|
21
|
+
newAgents: number;
|
|
22
|
+
totalAgents: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface UpsertResult {
|
|
26
|
+
inserted: number;
|
|
27
|
+
updated: number;
|
|
28
|
+
totalAgents: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ensureClanColor(clan: string): void {
|
|
32
|
+
const db = getDb();
|
|
33
|
+
const existing = db.prepare('SELECT id FROM clans WHERE id = ?').get(clan);
|
|
34
|
+
if (existing) return;
|
|
35
|
+
|
|
36
|
+
const clanCount = (db.prepare('SELECT COUNT(*) as c FROM clans').get() as { c: number }).c;
|
|
37
|
+
const color = CLAN_COLORS[clanCount % CLAN_COLORS.length]!;
|
|
38
|
+
db.prepare('INSERT INTO clans (id, name, color) VALUES (?, ?, ?)').run(clan, clan, color);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function upsertAgentRecord(agent: CollectedAgent, emitBornEvent: boolean): 'inserted' | 'updated' {
|
|
42
|
+
const db = getDb();
|
|
43
|
+
const existing = db.prepare('SELECT id FROM agents WHERE id = ?').get(agent.id);
|
|
44
|
+
const name = generateName(agent.id);
|
|
45
|
+
const maturity = computeMaturity(agent.total_tokens);
|
|
46
|
+
|
|
47
|
+
ensureClanColor(agent.clan);
|
|
48
|
+
|
|
49
|
+
db.prepare(`
|
|
50
|
+
INSERT INTO agents (
|
|
51
|
+
id, tool, model, project, clan, created_at,
|
|
52
|
+
total_tokens, input_tokens, output_tokens, cache_tokens, reasoning_tokens,
|
|
53
|
+
duration_ms, message_count,
|
|
54
|
+
trait_builder, trait_creator, trait_explorer, trait_leader, trait_thinker, trait_scholar,
|
|
55
|
+
name, maturity
|
|
56
|
+
) VALUES (
|
|
57
|
+
@id, @tool, @model, @project, @clan, @created_at,
|
|
58
|
+
@total_tokens, @input_tokens, @output_tokens, @cache_tokens, @reasoning_tokens,
|
|
59
|
+
@duration_ms, @message_count,
|
|
60
|
+
@trait_builder, @trait_creator, @trait_explorer, @trait_leader, @trait_thinker, @trait_scholar,
|
|
61
|
+
@name, @maturity
|
|
62
|
+
)
|
|
63
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
64
|
+
tool = excluded.tool,
|
|
65
|
+
model = excluded.model,
|
|
66
|
+
project = excluded.project,
|
|
67
|
+
clan = excluded.clan,
|
|
68
|
+
created_at = excluded.created_at,
|
|
69
|
+
total_tokens = excluded.total_tokens,
|
|
70
|
+
input_tokens = excluded.input_tokens,
|
|
71
|
+
output_tokens = excluded.output_tokens,
|
|
72
|
+
cache_tokens = excluded.cache_tokens,
|
|
73
|
+
reasoning_tokens = excluded.reasoning_tokens,
|
|
74
|
+
duration_ms = excluded.duration_ms,
|
|
75
|
+
message_count = excluded.message_count,
|
|
76
|
+
trait_builder = excluded.trait_builder,
|
|
77
|
+
trait_creator = excluded.trait_creator,
|
|
78
|
+
trait_explorer = excluded.trait_explorer,
|
|
79
|
+
trait_leader = excluded.trait_leader,
|
|
80
|
+
trait_thinker = excluded.trait_thinker,
|
|
81
|
+
trait_scholar = excluded.trait_scholar,
|
|
82
|
+
name = excluded.name,
|
|
83
|
+
maturity = excluded.maturity
|
|
84
|
+
`).run({ ...agent, name, maturity });
|
|
85
|
+
|
|
86
|
+
if (!existing && emitBornEvent) {
|
|
87
|
+
db.prepare(
|
|
88
|
+
'INSERT INTO events (timestamp, agent_id, type, description) VALUES (?, ?, ?, ?)'
|
|
89
|
+
).run(
|
|
90
|
+
agent.created_at,
|
|
91
|
+
agent.id,
|
|
92
|
+
'born',
|
|
93
|
+
`${name} arrived from a new session`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return existing ? 'updated' : 'inserted';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function upsertAgents(
|
|
101
|
+
agents: CollectedAgent[],
|
|
102
|
+
options: { emitBornEvents?: boolean } = {},
|
|
103
|
+
): UpsertResult {
|
|
104
|
+
const db = getDb();
|
|
105
|
+
let inserted = 0;
|
|
106
|
+
let updated = 0;
|
|
107
|
+
|
|
108
|
+
const batch = db.transaction(() => {
|
|
109
|
+
for (const agent of agents) {
|
|
110
|
+
const outcome = upsertAgentRecord(agent, options.emitBornEvents ?? false);
|
|
111
|
+
if (outcome === 'inserted') inserted++;
|
|
112
|
+
else updated++;
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
batch();
|
|
117
|
+
|
|
118
|
+
const totalAgents = (db.prepare('SELECT COUNT(*) as c FROM agents').get() as { c: number }).c;
|
|
119
|
+
return { inserted, updated, totalAgents };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function mergeRemoteState(remote: PullResponse): UpsertResult {
|
|
123
|
+
setWorldConfig(remote.world);
|
|
124
|
+
return upsertAgents(remote.agents, { emitBornEvents: false });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function getAllAgents(): CollectedAgent[] {
|
|
128
|
+
return getDb().prepare(`
|
|
129
|
+
SELECT
|
|
130
|
+
id, tool, model, project, clan, created_at,
|
|
131
|
+
total_tokens, input_tokens, output_tokens, cache_tokens, reasoning_tokens,
|
|
132
|
+
duration_ms, message_count,
|
|
133
|
+
trait_builder, trait_creator, trait_explorer, trait_leader, trait_thinker, trait_scholar
|
|
134
|
+
FROM agents
|
|
135
|
+
ORDER BY created_at
|
|
136
|
+
`).all() as CollectedAgent[];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function syncAll(opts: SyncOptions = {}): SyncResult {
|
|
140
|
+
const claudeDir = opts.claudeDir ?? path.join(os.homedir(), '.claude');
|
|
141
|
+
const codexDir = opts.codexDir ?? path.join(os.homedir(), '.codex');
|
|
142
|
+
|
|
143
|
+
const claudeAgents = scanClaudeCode(claudeDir);
|
|
144
|
+
const codexAgents = scanCodexCli(codexDir);
|
|
145
|
+
const upsert = upsertAgents([...claudeAgents, ...codexAgents], { emitBornEvents: true });
|
|
146
|
+
|
|
147
|
+
return { newAgents: upsert.inserted, totalAgents: upsert.totalAgents };
|
|
148
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { CollectedAgent, PullResponse, UserWorldConfig } from '@vsuryav/agent-sim-contracts';
|