@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
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# agent sim
|
|
2
|
+
|
|
3
|
+
a cute pastel idle world that grows from your agentic coding sessions.
|
|
4
|
+
|
|
5
|
+
## install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @vsuryav/agent-sim
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## first run
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
agent-sim login https://degenerately-muscleless-athena.ngrok-free.dev
|
|
15
|
+
agent-sim
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
the app will:
|
|
19
|
+
|
|
20
|
+
- scan your local coding sessions
|
|
21
|
+
- rebuild your world locally
|
|
22
|
+
- pull your synced agents from the server
|
|
23
|
+
- keep pushing new state while the app is open
|
|
24
|
+
|
|
25
|
+
legacy `agent-wars` local data is migrated automatically to `~/.agent-sim` on first run.
|
package/bin/agent-sim.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const cliEntry = fileURLToPath(new URL('../src/cli.ts', import.meta.url));
|
|
7
|
+
|
|
8
|
+
const child = spawn(
|
|
9
|
+
process.execPath,
|
|
10
|
+
['--import', 'tsx', cliEntry, ...process.argv.slice(2)],
|
|
11
|
+
{ stdio: 'inherit' },
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
child.on('exit', (code, signal) => {
|
|
15
|
+
if (signal) {
|
|
16
|
+
process.kill(process.pid, signal);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
process.exit(code ?? 0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
child.on('error', (error) => {
|
|
23
|
+
console.error(error);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vsuryav/agent-sim",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local TUI and sync client for Agent Sim",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"bin",
|
|
8
|
+
"src",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
11
|
+
"bin": {
|
|
12
|
+
"agent-sim": "bin/agent-sim.js",
|
|
13
|
+
"agent-wars": "bin/agent-sim.js"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/vsuryav/agent-wars.git"
|
|
18
|
+
},
|
|
19
|
+
"homepage": "https://github.com/vsuryav/agent-wars",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/vsuryav/agent-wars/issues"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"scripts": {
|
|
27
|
+
"dev": "tsx src/main.tsx",
|
|
28
|
+
"start": "node dist/main.js",
|
|
29
|
+
"test": "vitest run src",
|
|
30
|
+
"test:watch": "vitest src"
|
|
31
|
+
},
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=20"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@vsuryav/agent-sim-contracts": "^0.1.0",
|
|
37
|
+
"@vsuryav/agent-sim-core": "^0.1.0",
|
|
38
|
+
"@alcalzone/ansi-tokenize": "^0.1.3",
|
|
39
|
+
"auto-bind": "^5.0.1",
|
|
40
|
+
"better-sqlite3": "^11.0.0",
|
|
41
|
+
"bidi-js": "^1.0.3",
|
|
42
|
+
"chalk": "^5.4.0",
|
|
43
|
+
"cli-boxes": "^3.0.0",
|
|
44
|
+
"code-excerpt": "^4.0.0",
|
|
45
|
+
"emoji-regex": "^10.4.0",
|
|
46
|
+
"figures": "^6.1.0",
|
|
47
|
+
"get-east-asian-width": "^1.3.0",
|
|
48
|
+
"indent-string": "^5.0.0",
|
|
49
|
+
"lodash-es": "^4.17.21",
|
|
50
|
+
"react": "^19.0.0",
|
|
51
|
+
"react-reconciler": "^0.31.0",
|
|
52
|
+
"semver": "^7.6.0",
|
|
53
|
+
"signal-exit": "^4.1.0",
|
|
54
|
+
"stack-utils": "^2.0.6",
|
|
55
|
+
"strip-ansi": "^7.1.0",
|
|
56
|
+
"supports-hyperlinks": "^3.1.0",
|
|
57
|
+
"tsx": "^4.19.0",
|
|
58
|
+
"type-fest": "^4.30.0",
|
|
59
|
+
"usehooks-ts": "^3.1.0",
|
|
60
|
+
"wrap-ansi": "^9.0.0",
|
|
61
|
+
"ws": "^8.18.0"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/better-sqlite3": "^7.6.11",
|
|
65
|
+
"@types/lodash-es": "^4.17.12",
|
|
66
|
+
"@types/react": "^19.0.0",
|
|
67
|
+
"@types/react-reconciler": "^0.28.0",
|
|
68
|
+
"@types/ws": "^8.5.12",
|
|
69
|
+
"typescript": "^5.7.0",
|
|
70
|
+
"vitest": "^4.1.3"
|
|
71
|
+
}
|
|
72
|
+
}
|
package/src/app-paths.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
export const APP_NAME = 'agent-sim';
|
|
6
|
+
export const APP_TITLE = 'Agent Sim';
|
|
7
|
+
export const LEGACY_APP_NAME = 'agent-wars';
|
|
8
|
+
|
|
9
|
+
export const DATA_DIR = path.join(os.homedir(), `.${APP_NAME}`);
|
|
10
|
+
export const LEGACY_DATA_DIR = path.join(os.homedir(), `.${LEGACY_APP_NAME}`);
|
|
11
|
+
export const DB_PATH = path.join(DATA_DIR, 'db.sqlite');
|
|
12
|
+
export const CONFIG_PATH = path.join(DATA_DIR, 'config.json');
|
|
13
|
+
|
|
14
|
+
function pathExists(target: string): boolean {
|
|
15
|
+
try {
|
|
16
|
+
fs.accessSync(target);
|
|
17
|
+
return true;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ensureLocalDataDir(): void {
|
|
24
|
+
if (!pathExists(DATA_DIR) && pathExists(LEGACY_DATA_DIR)) {
|
|
25
|
+
fs.renameSync(LEGACY_DATA_DIR, DATA_DIR);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
29
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { closeDb } from './collector/db.js';
|
|
3
|
+
|
|
4
|
+
const syncAllMock = vi.fn();
|
|
5
|
+
const getAllAgentsMock = vi.fn();
|
|
6
|
+
const hasRemoteConfigMock = vi.fn();
|
|
7
|
+
const pushToServerMock = vi.fn();
|
|
8
|
+
const pullFromServerMock = vi.fn();
|
|
9
|
+
|
|
10
|
+
vi.mock('./collector/sync.js', async () => {
|
|
11
|
+
const actual = await vi.importActual<typeof import('./collector/sync.js')>('./collector/sync.js');
|
|
12
|
+
return {
|
|
13
|
+
...actual,
|
|
14
|
+
syncAll: syncAllMock,
|
|
15
|
+
getAllAgents: getAllAgentsMock,
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
vi.mock('./collector/remote-sync.js', () => ({
|
|
20
|
+
hasRemoteConfig: hasRemoteConfigMock,
|
|
21
|
+
pushToServer: pushToServerMock,
|
|
22
|
+
pullFromServer: pullFromServerMock,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('startPeriodicRemoteSync', () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
vi.useFakeTimers();
|
|
28
|
+
vi.clearAllMocks();
|
|
29
|
+
hasRemoteConfigMock.mockReturnValue(true);
|
|
30
|
+
syncAllMock.mockReturnValue({ newAgents: 1, totalAgents: 3 });
|
|
31
|
+
getAllAgentsMock.mockReturnValue([{ id: 'agent-1' }]);
|
|
32
|
+
pushToServerMock.mockResolvedValue({ synced: 3 });
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
closeDb();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('pushes periodically while the app is running', async () => {
|
|
41
|
+
const { startPeriodicRemoteSync } = await import('./app-sync.js');
|
|
42
|
+
const onSync = vi.fn();
|
|
43
|
+
const controller = startPeriodicRemoteSync({ intervalMs: 1_000, onSync });
|
|
44
|
+
|
|
45
|
+
expect(controller).toBeTruthy();
|
|
46
|
+
|
|
47
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
48
|
+
|
|
49
|
+
expect(syncAllMock).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(pushToServerMock).toHaveBeenCalledTimes(1);
|
|
51
|
+
expect(onSync).toHaveBeenCalledWith({
|
|
52
|
+
local: { newAgents: 1, totalAgents: 3 },
|
|
53
|
+
remotePush: { synced: 3 },
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
await controller!.stop();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('flushes one final push when stopped', async () => {
|
|
60
|
+
const { startPeriodicRemoteSync } = await import('./app-sync.js');
|
|
61
|
+
const controller = startPeriodicRemoteSync({ intervalMs: 60_000 });
|
|
62
|
+
|
|
63
|
+
await controller!.stop(true);
|
|
64
|
+
|
|
65
|
+
expect(syncAllMock).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(pushToServerMock).toHaveBeenCalledTimes(1);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('does not start when remote sync is not configured', async () => {
|
|
70
|
+
hasRemoteConfigMock.mockReturnValue(false);
|
|
71
|
+
const { startPeriodicRemoteSync } = await import('./app-sync.js');
|
|
72
|
+
|
|
73
|
+
expect(startPeriodicRemoteSync()).toBeNull();
|
|
74
|
+
});
|
|
75
|
+
});
|
package/src/app-sync.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { getAllAgents, mergeRemoteState, syncAll, type SyncResult, type UpsertResult } from './collector/sync.js';
|
|
2
|
+
import { hasRemoteConfig, pullFromServer, pushToServer } from './collector/remote-sync.js';
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_PERIODIC_REMOTE_SYNC_MS = 60_000;
|
|
5
|
+
|
|
6
|
+
export interface StartupSyncResult {
|
|
7
|
+
local: SyncResult;
|
|
8
|
+
remotePull?: {
|
|
9
|
+
remoteAgents: number;
|
|
10
|
+
merge: UpsertResult;
|
|
11
|
+
githubLogin: string;
|
|
12
|
+
};
|
|
13
|
+
remotePush?: {
|
|
14
|
+
synced: number;
|
|
15
|
+
};
|
|
16
|
+
remoteError?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function runStartupSync(): Promise<StartupSyncResult> {
|
|
20
|
+
const local = syncAll();
|
|
21
|
+
const result: StartupSyncResult = { local };
|
|
22
|
+
|
|
23
|
+
if (!hasRemoteConfig()) return result;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const remote = await pullFromServer();
|
|
27
|
+
result.remotePull = {
|
|
28
|
+
remoteAgents: remote.agents.length,
|
|
29
|
+
merge: mergeRemoteState(remote),
|
|
30
|
+
githubLogin: remote.world.github_login,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
result.remotePush = await pushToServer(getAllAgents());
|
|
34
|
+
} catch (error) {
|
|
35
|
+
result.remoteError = error instanceof Error ? error.message : String(error);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return result;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function runRemotePush(): Promise<{ synced: number } | null> {
|
|
42
|
+
if (!hasRemoteConfig()) return null;
|
|
43
|
+
syncAll();
|
|
44
|
+
return pushToServer(getAllAgents());
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface PeriodicRemoteSyncUpdate {
|
|
48
|
+
local: SyncResult;
|
|
49
|
+
remotePush: {
|
|
50
|
+
synced: number;
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PeriodicRemoteSyncController {
|
|
55
|
+
stop(flush?: boolean): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function startPeriodicRemoteSync(options: {
|
|
59
|
+
intervalMs?: number;
|
|
60
|
+
onError?: (message: string) => void;
|
|
61
|
+
onSync?: (update: PeriodicRemoteSyncUpdate) => void;
|
|
62
|
+
} = {}): PeriodicRemoteSyncController | null {
|
|
63
|
+
if (!hasRemoteConfig()) return null;
|
|
64
|
+
|
|
65
|
+
const intervalMs = options.intervalMs ?? DEFAULT_PERIODIC_REMOTE_SYNC_MS;
|
|
66
|
+
let stopped = false;
|
|
67
|
+
let inFlight: Promise<void> | null = null;
|
|
68
|
+
|
|
69
|
+
const runTick = async (force = false) => {
|
|
70
|
+
if ((!force && stopped) || inFlight) return inFlight;
|
|
71
|
+
|
|
72
|
+
inFlight = (async () => {
|
|
73
|
+
try {
|
|
74
|
+
const local = syncAll();
|
|
75
|
+
const remotePush = await pushToServer(getAllAgents());
|
|
76
|
+
options.onSync?.({ local, remotePush });
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
options.onError?.(message);
|
|
80
|
+
} finally {
|
|
81
|
+
inFlight = null;
|
|
82
|
+
}
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
return inFlight;
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const timer = setInterval(() => {
|
|
89
|
+
void runTick();
|
|
90
|
+
}, intervalMs);
|
|
91
|
+
timer.unref?.();
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
async stop(flush = false) {
|
|
95
|
+
if (stopped) {
|
|
96
|
+
if (flush && inFlight) await inFlight;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
clearInterval(timer);
|
|
101
|
+
stopped = true;
|
|
102
|
+
|
|
103
|
+
if (flush) {
|
|
104
|
+
await runTick(true);
|
|
105
|
+
} else if (inFlight) {
|
|
106
|
+
await inFlight;
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { initDb, getDb, closeDb } from './collector/db.js';
|
|
3
|
+
import { mergeRemoteState, syncAll } from './collector/sync.js';
|
|
4
|
+
import { DEFAULT_SERVER_URL, getPreferredServerUrl, login, pullFromServer, setToken } from './collector/remote-sync.js';
|
|
5
|
+
import { runRemotePush } from './app-sync.js';
|
|
6
|
+
import { APP_NAME, DB_PATH, ensureLocalDataDir, LEGACY_APP_NAME } from './app-paths.js';
|
|
7
|
+
import { printOnboardingHint, printStartupSyncSummary, runInteractiveSession } from './run-interactive.js';
|
|
8
|
+
let dbInitialized = false;
|
|
9
|
+
|
|
10
|
+
function ensureDataDir() {
|
|
11
|
+
ensureLocalDataDir();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function ensureDb() {
|
|
15
|
+
if (dbInitialized) return;
|
|
16
|
+
ensureDataDir();
|
|
17
|
+
initDb(DB_PATH);
|
|
18
|
+
dbInitialized = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printStatus() {
|
|
22
|
+
const db = getDb();
|
|
23
|
+
const agentCount = (db.prepare('SELECT COUNT(*) as c FROM agents').get() as any).c;
|
|
24
|
+
const clanCount = (db.prepare('SELECT COUNT(*) as c FROM clans').get() as any).c;
|
|
25
|
+
const phase = (db.prepare("SELECT value FROM world WHERE key = 'phase'").get() as any)?.value ?? 'unknown';
|
|
26
|
+
|
|
27
|
+
console.log(`\n ${APP_NAME} status`);
|
|
28
|
+
console.log(` ─────────────────`);
|
|
29
|
+
console.log(` Phase: ${phase}`);
|
|
30
|
+
console.log(` Agents: ${agentCount}`);
|
|
31
|
+
console.log(` Clans: ${clanCount}`);
|
|
32
|
+
|
|
33
|
+
const clans = db.prepare('SELECT * FROM clans ORDER BY name').all() as any[];
|
|
34
|
+
for (const clan of clans) {
|
|
35
|
+
const members = db.prepare(
|
|
36
|
+
'SELECT name, maturity, tool, total_tokens FROM agents WHERE clan = ? ORDER BY created_at'
|
|
37
|
+
).all(clan.name) as any[];
|
|
38
|
+
console.log(`\n [${clan.name}] (${members.length} agents)`);
|
|
39
|
+
for (const m of members) {
|
|
40
|
+
const tokens = m.total_tokens >= 1_000_000
|
|
41
|
+
? `${(m.total_tokens / 1_000_000).toFixed(1)}M`
|
|
42
|
+
: m.total_tokens >= 1_000
|
|
43
|
+
? `${(m.total_tokens / 1_000).toFixed(0)}K`
|
|
44
|
+
: `${m.total_tokens}`;
|
|
45
|
+
console.log(` ${m.name} — ${m.maturity} · ${tokens} tokens`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
console.log('');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printHelp() {
|
|
52
|
+
const serverUrl = getPreferredServerUrl();
|
|
53
|
+
|
|
54
|
+
console.log(`\n ${APP_NAME}`);
|
|
55
|
+
console.log(' ──────────');
|
|
56
|
+
console.log(' whimsical local TUI for your agentic coding life\n');
|
|
57
|
+
console.log(' Commands:');
|
|
58
|
+
console.log(` ${APP_NAME} Start the game`);
|
|
59
|
+
console.log(` ${APP_NAME} login [url] Login for cross-machine sync (default: ${DEFAULT_SERVER_URL})`);
|
|
60
|
+
console.log(` ${APP_NAME} sync Scan local sessions, pull remote agents, push local agents`);
|
|
61
|
+
console.log(` ${APP_NAME} pull Pull remote agents only`);
|
|
62
|
+
console.log(` ${APP_NAME} push Push local agents only`);
|
|
63
|
+
console.log(` ${APP_NAME} status Print local world summary`);
|
|
64
|
+
console.log(` ${APP_NAME} help Show this help\n`);
|
|
65
|
+
console.log(' First run:');
|
|
66
|
+
console.log(` ${APP_NAME} login ${serverUrl}`);
|
|
67
|
+
console.log(` ${APP_NAME}\n`);
|
|
68
|
+
console.log(` Legacy alias: ${LEGACY_APP_NAME}\n`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const command = process.argv[2];
|
|
72
|
+
|
|
73
|
+
async function main() {
|
|
74
|
+
try {
|
|
75
|
+
if (command === 'help' || command === '--help' || command === '-h') {
|
|
76
|
+
printHelp();
|
|
77
|
+
} else if (command === 'login') {
|
|
78
|
+
const serverUrl = process.argv[3] ?? DEFAULT_SERVER_URL;
|
|
79
|
+
await login(serverUrl);
|
|
80
|
+
} else if (command === 'set-token') {
|
|
81
|
+
const token = process.argv[3];
|
|
82
|
+
if (!token) { console.error(`Usage: ${APP_NAME} set-token <token>`); process.exit(1); }
|
|
83
|
+
setToken(token);
|
|
84
|
+
} else if (command === 'pull') {
|
|
85
|
+
ensureDb();
|
|
86
|
+
const remote = await pullFromServer();
|
|
87
|
+
const merge = mergeRemoteState(remote);
|
|
88
|
+
console.log(` Pulled ${remote.agents.length} agent(s) for ${remote.world.github_login}.`);
|
|
89
|
+
console.log(` Added ${merge.inserted} new local agent(s), updated ${merge.updated}.`);
|
|
90
|
+
} else if (command === 'push') {
|
|
91
|
+
ensureDb();
|
|
92
|
+
const pushResult = await runRemotePush();
|
|
93
|
+
if (!pushResult) {
|
|
94
|
+
throw new Error(`Not logged in. Run: ${APP_NAME} login <server-url>`);
|
|
95
|
+
}
|
|
96
|
+
console.log(` Pushed ${pushResult.synced} agents to server.`);
|
|
97
|
+
} else {
|
|
98
|
+
if (command === 'sync') {
|
|
99
|
+
ensureDb();
|
|
100
|
+
const { runStartupSync } = await import('./app-sync.js');
|
|
101
|
+
const result = await runStartupSync();
|
|
102
|
+
printStartupSyncSummary(result);
|
|
103
|
+
} else if (command === 'status') {
|
|
104
|
+
ensureDb();
|
|
105
|
+
const result = syncAll();
|
|
106
|
+
if (result.newAgents > 0) {
|
|
107
|
+
console.log(` Synced ${result.newAgents} new agent(s). Total: ${result.totalAgents}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (command === 'status' || command === 'sync') {
|
|
112
|
+
printStatus();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!command) {
|
|
116
|
+
ensureDb();
|
|
117
|
+
await runInteractiveSession();
|
|
118
|
+
} else if (command) {
|
|
119
|
+
console.error(`Unknown command: ${command}`);
|
|
120
|
+
printHelp();
|
|
121
|
+
process.exitCode = 1;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
} finally {
|
|
125
|
+
closeDb();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
main();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { scanClaudeCode } from './claude-code.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('Claude Code collector', () => {
|
|
9
|
+
const testDir = path.join(os.tmpdir(), `agent-sim-cc-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-project'), { recursive: true });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
closeDb();
|
|
22
|
+
fs.rmSync(testDir, { recursive: true, force: true });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('discovers sessions from session files', () => {
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(mockClaudeDir, 'sessions', '1234.json'),
|
|
28
|
+
JSON.stringify({
|
|
29
|
+
pid: 1234,
|
|
30
|
+
sessionId: 'test-uuid-1',
|
|
31
|
+
cwd: '/test/project',
|
|
32
|
+
startedAt: 1700000000000,
|
|
33
|
+
kind: 'interactive',
|
|
34
|
+
entrypoint: 'cli',
|
|
35
|
+
})
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const convLines = [
|
|
39
|
+
JSON.stringify({
|
|
40
|
+
type: 'summary',
|
|
41
|
+
sessionId: 'test-uuid-1',
|
|
42
|
+
parentUuid: null,
|
|
43
|
+
}),
|
|
44
|
+
JSON.stringify({
|
|
45
|
+
parentUuid: 'root',
|
|
46
|
+
type: 'message',
|
|
47
|
+
message: {
|
|
48
|
+
role: 'assistant',
|
|
49
|
+
model: 'claude-opus-4-6',
|
|
50
|
+
content: [
|
|
51
|
+
{ type: 'text', text: 'hello' },
|
|
52
|
+
{ type: 'tool_use', name: 'Bash', id: 't1', input: {} },
|
|
53
|
+
],
|
|
54
|
+
usage: {
|
|
55
|
+
input_tokens: 100,
|
|
56
|
+
output_tokens: 200,
|
|
57
|
+
cache_read_input_tokens: 1000,
|
|
58
|
+
cache_creation_input_tokens: 500,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
uuid: 'msg-1',
|
|
62
|
+
timestamp: 1700000001000,
|
|
63
|
+
sessionId: 'test-uuid-1',
|
|
64
|
+
}),
|
|
65
|
+
];
|
|
66
|
+
fs.writeFileSync(
|
|
67
|
+
path.join(mockClaudeDir, 'projects', '-test-project', 'test-uuid-1.jsonl'),
|
|
68
|
+
convLines.join('\n')
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const result = scanClaudeCode(mockClaudeDir);
|
|
72
|
+
|
|
73
|
+
expect(result.length).toBe(1);
|
|
74
|
+
expect(result[0].id).toBe('test-uuid-1');
|
|
75
|
+
expect(result[0].tool).toBe('claude-code');
|
|
76
|
+
expect(result[0].model).toBe('claude-opus-4-6');
|
|
77
|
+
expect(result[0].input_tokens).toBe(100);
|
|
78
|
+
expect(result[0].output_tokens).toBe(200);
|
|
79
|
+
expect(result[0].cache_tokens).toBe(1500);
|
|
80
|
+
expect(result[0].trait_builder).toBeGreaterThan(0);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('skips sessions already in the database', () => {
|
|
84
|
+
const db = getDb();
|
|
85
|
+
db.prepare(`INSERT INTO agents (id, tool, project, clan, created_at)
|
|
86
|
+
VALUES (?, ?, ?, ?, ?)`).run('test-uuid-1', 'claude-code', '/test/project', 'project', Date.now());
|
|
87
|
+
|
|
88
|
+
fs.writeFileSync(
|
|
89
|
+
path.join(mockClaudeDir, 'sessions', '1234.json'),
|
|
90
|
+
JSON.stringify({
|
|
91
|
+
pid: 1234,
|
|
92
|
+
sessionId: 'test-uuid-1',
|
|
93
|
+
cwd: '/test/project',
|
|
94
|
+
startedAt: 1700000000000,
|
|
95
|
+
kind: 'interactive',
|
|
96
|
+
})
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const result = scanClaudeCode(mockClaudeDir);
|
|
100
|
+
expect(result.length).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
});
|