@mod-computer/cli 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.
Files changed (56) hide show
  1. package/README.md +125 -0
  2. package/commands/execute.md +156 -0
  3. package/commands/overview.md +233 -0
  4. package/commands/review.md +151 -0
  5. package/commands/spec.md +169 -0
  6. package/dist/app.js +227 -0
  7. package/dist/cli.bundle.js +25824 -0
  8. package/dist/cli.bundle.js.map +7 -0
  9. package/dist/cli.js +121 -0
  10. package/dist/commands/agents-run.js +71 -0
  11. package/dist/commands/auth.js +151 -0
  12. package/dist/commands/branch.js +1411 -0
  13. package/dist/commands/claude-sync.js +772 -0
  14. package/dist/commands/index.js +43 -0
  15. package/dist/commands/init.js +378 -0
  16. package/dist/commands/recover.js +207 -0
  17. package/dist/commands/spec.js +386 -0
  18. package/dist/commands/status.js +329 -0
  19. package/dist/commands/sync.js +95 -0
  20. package/dist/commands/workspace.js +423 -0
  21. package/dist/components/conflict-resolution-ui.js +120 -0
  22. package/dist/components/messages.js +5 -0
  23. package/dist/components/thread.js +8 -0
  24. package/dist/config/features.js +72 -0
  25. package/dist/config/release-profiles/development.json +11 -0
  26. package/dist/config/release-profiles/mvp.json +12 -0
  27. package/dist/config/release-profiles/v0.1.json +11 -0
  28. package/dist/config/release-profiles/v0.2.json +11 -0
  29. package/dist/containers/branches-container.js +140 -0
  30. package/dist/containers/directory-container.js +92 -0
  31. package/dist/containers/thread-container.js +214 -0
  32. package/dist/containers/threads-container.js +27 -0
  33. package/dist/containers/workspaces-container.js +27 -0
  34. package/dist/daemon-worker.js +257 -0
  35. package/dist/lib/auth-server.js +153 -0
  36. package/dist/lib/browser.js +35 -0
  37. package/dist/lib/storage.js +203 -0
  38. package/dist/services/automatic-file-tracker.js +303 -0
  39. package/dist/services/cli-orchestrator.js +227 -0
  40. package/dist/services/feature-flags.js +187 -0
  41. package/dist/services/file-import-service.js +283 -0
  42. package/dist/services/file-transformation-service.js +218 -0
  43. package/dist/services/logger.js +44 -0
  44. package/dist/services/mod-config.js +61 -0
  45. package/dist/services/modignore-service.js +326 -0
  46. package/dist/services/sync-daemon.js +244 -0
  47. package/dist/services/thread-notification-service.js +50 -0
  48. package/dist/services/thread-service.js +147 -0
  49. package/dist/stores/use-directory-store.js +96 -0
  50. package/dist/stores/use-threads-store.js +46 -0
  51. package/dist/stores/use-workspaces-store.js +32 -0
  52. package/dist/types/config.js +16 -0
  53. package/dist/types/index.js +2 -0
  54. package/dist/types/workspace-connection.js +2 -0
  55. package/dist/types.js +1 -0
  56. package/package.json +67 -0
@@ -0,0 +1,50 @@
1
+ export class ThreadNotificationService {
2
+ constructor(repo) {
3
+ this.repo = repo;
4
+ }
5
+ /**
6
+ * Add a file update notification to the thread
7
+ * For now, this just logs the notification - can be enhanced later
8
+ */
9
+ async addFileUpdateNotification(threadId, fileName, action) {
10
+ try {
11
+ const message = this.getNotificationMessage(fileName, action);
12
+ // For now, just log the notification
13
+ // TODO: Later integrate with actual thread message system
14
+ console.log(`📢 ${message}`);
15
+ }
16
+ catch (error) {
17
+ console.error(`Failed to add file update notification:`, error);
18
+ }
19
+ }
20
+ /**
21
+ * Generate notification message based on action
22
+ */
23
+ getNotificationMessage(fileName, action) {
24
+ const emoji = this.getActionEmoji(action);
25
+ const verb = this.getActionVerb(action);
26
+ return `${emoji} ${fileName} ${verb}`;
27
+ }
28
+ /**
29
+ * Get emoji for action type
30
+ */
31
+ getActionEmoji(action) {
32
+ switch (action) {
33
+ case 'updated': return '📝';
34
+ case 'created': return '✨';
35
+ case 'deleted': return '🗑️';
36
+ default: return '📄';
37
+ }
38
+ }
39
+ /**
40
+ * Get verb for action type
41
+ */
42
+ getActionVerb(action) {
43
+ switch (action) {
44
+ case 'updated': return 'updated';
45
+ case 'created': return 'created';
46
+ case 'deleted': return 'deleted';
47
+ default: return 'changed';
48
+ }
49
+ }
50
+ }
@@ -0,0 +1,147 @@
1
+ import { ThreadService as CoreThreadService } from '@mod/mod-core/services/thread-service';
2
+ import { BranchService } from '@mod/mod-core/services/branch-service';
3
+ import { chatWithAgentCli } from './cli-orchestrator.js';
4
+ import { log } from './logger.js';
5
+ export class CLIThreadService {
6
+ // private ai: typeof runAgentWithStream; // Placeholder for orchestrator/agent logic
7
+ constructor(repo) {
8
+ this.core = new CoreThreadService(repo);
9
+ this.repo = repo;
10
+ // this.ai = runAgentWithStream; // Assign orchestrator/agent logic as needed
11
+ }
12
+ async getThreads(workspaceId) {
13
+ const wsHandle = await this.repo.find(workspaceId);
14
+ const workspace = (await wsHandle.doc());
15
+ if (!workspace || !workspace.branchesDocId)
16
+ return [];
17
+ const branchService = new BranchService(this.repo);
18
+ const threads = [];
19
+ try {
20
+ const branches = await branchService.getBranchesForWorkspace(workspace.branchesDocId);
21
+ for (const branch of branches) {
22
+ const threadId = branch?.threadId;
23
+ if (!threadId)
24
+ continue;
25
+ try {
26
+ const thread = await this.core.getThread(threadId);
27
+ if (thread)
28
+ threads.push(thread);
29
+ }
30
+ catch (err) {
31
+ log('[CLIThreadService] Failed to load thread', {
32
+ branchId: branch?.id,
33
+ threadId,
34
+ err,
35
+ });
36
+ }
37
+ }
38
+ }
39
+ catch (err) {
40
+ log('[CLIThreadService] Failed to load branches for workspace', {
41
+ workspaceId,
42
+ err,
43
+ });
44
+ }
45
+ return threads;
46
+ }
47
+ async getThread(threadId) {
48
+ return this.core.getThread(threadId);
49
+ }
50
+ async createThread(name, branchId, workspaceId) {
51
+ const threadId = await this.core.createThread(name, branchId);
52
+ try {
53
+ const branchService = new BranchService(this.repo);
54
+ const wsHandle = await this.repo.find(workspaceId);
55
+ const workspace = (await wsHandle.doc());
56
+ if (workspace?.branchesDocId) {
57
+ await branchService.setThreadId(branchId, threadId, workspace.branchesDocId);
58
+ }
59
+ wsHandle.change((doc) => {
60
+ doc.activeThreadId = threadId;
61
+ });
62
+ }
63
+ catch (err) {
64
+ log('[CLIThreadService] Failed to persist threadId on branch', {
65
+ branchId,
66
+ workspaceId,
67
+ err,
68
+ });
69
+ }
70
+ return threadId;
71
+ }
72
+ /**
73
+ * Stream agent chat for a thread in the CLI.
74
+ * Yields agent response chunks for UI updates.
75
+ */
76
+ async *streamAgentChat({ threadId, userMessage, user = { id: 'user-1', name: 'You', avatarUrl: '' }, workspace, agent, files = [], images = [], maxMessages = 20, ...opts }) {
77
+ // 1) Persist user message immediately
78
+ await this.core.addThreadItemToThread(threadId, userMessage, user.id, user.name, user.avatarUrl, { workspaceId: workspace.id });
79
+ // 2) Start CLI-native orchestrator stream
80
+ const apiKey = (process.env.OPENAI_API_KEY || process.env.OPENAI_API_TOKEN || process.env.OPENAI || process.env.ANTHROPIC_API_KEY || '').trim();
81
+ try {
82
+ try {
83
+ log('[CLIThreadService] Starting stream with agent:', {
84
+ name: agent?.name,
85
+ id: agent?.id,
86
+ defaultModel: agent?.defaultModel,
87
+ tools: agent?.tools ? Object.keys(agent.tools) : [],
88
+ });
89
+ }
90
+ catch { }
91
+ for await (const part of chatWithAgentCli({
92
+ repo: this.repo,
93
+ threadId,
94
+ userMessage,
95
+ user,
96
+ workspace,
97
+ agent: agent || { name: 'Assistant', instructions: 'Be helpful.' },
98
+ files,
99
+ images,
100
+ apiKey,
101
+ tools: (agent && agent.tools) || {},
102
+ })) {
103
+ const p = part;
104
+ if (p.type === 'content' && p.content) {
105
+ yield { type: 'content', content: p.content };
106
+ }
107
+ else if (p.type === 'tool-call') {
108
+ yield p;
109
+ }
110
+ else if (p.type === 'tool-result') {
111
+ yield { type: 'tool-result', output: p.output };
112
+ }
113
+ else if (p.type === 'error') {
114
+ yield { type: 'error', error: String(p.error) };
115
+ }
116
+ }
117
+ }
118
+ catch (err) {
119
+ const msg = `orchestrator import/stream failed: ${err?.message || String(err)}`;
120
+ yield { type: 'error', error: msg };
121
+ try {
122
+ await this.core.addAssistantMessageToThread(threadId, msg, agent?.name || 'Assistant');
123
+ }
124
+ catch { }
125
+ }
126
+ }
127
+ async sendUserMessage(threadId, message, agentConfig) {
128
+ // 1. Add user message to thread (core)
129
+ // const threadRepo = new ThreadRepository(this.repo);
130
+ // await threadRepo.addThreadItem(
131
+ // threadId,
132
+ // message,
133
+ // [], // contextDocumentIds
134
+ // true, // addToThread
135
+ // 'user-1', // userId
136
+ // 'You', // userName
137
+ // undefined, // userAvatarUrl
138
+ // {type: 'message'},
139
+ // );
140
+ // 2. TODO: Start agent stream (mod-ai)
141
+ // 3. TODO: As agent responses stream in, append to thread
142
+ // 4. TODO: Notify UI of updates (if needed)
143
+ }
144
+ subscribeToThread(threadId, callback) {
145
+ // TODO: Implement Automerge doc subscription for reactivity
146
+ }
147
+ }
@@ -0,0 +1,96 @@
1
+ import { createStore } from 'zustand';
2
+ import { useStore } from 'zustand';
3
+ import { BranchableRepo } from '@mod/mod-core/services/branchable-repo';
4
+ import { readModConfig } from '../services/mod-config.js';
5
+ export const directoryStore = createStore((set, get) => ({
6
+ items: {},
7
+ rootIds: [],
8
+ currentPath: [],
9
+ expandedFolders: {},
10
+ loading: false,
11
+ error: null,
12
+ setItems: (items, rootIds) => set(() => ({
13
+ items: Object.fromEntries(items.map((i) => [i.id, i])),
14
+ rootIds,
15
+ })),
16
+ setCurrentPath: (path) => set(() => ({ currentPath: path })),
17
+ setExpanded: (folderId, expanded) => set((state) => ({
18
+ expandedFolders: { ...state.expandedFolders, [folderId]: expanded }
19
+ })),
20
+ setLoading: (loading) => set(() => ({ loading })),
21
+ setError: (error) => set(() => ({ error })),
22
+ reset: () => set(() => ({
23
+ items: {},
24
+ rootIds: [],
25
+ currentPath: [],
26
+ expandedFolders: {},
27
+ loading: false,
28
+ error: null,
29
+ })),
30
+ async loadRootDirectory({ repo, selectedWorkspace, selectedThread }) {
31
+ set({ loading: true, error: null });
32
+ try {
33
+ if (!repo) {
34
+ set({ error: 'No repo available', loading: false });
35
+ return;
36
+ }
37
+ const effectiveWorkspaceId = (selectedWorkspace && selectedWorkspace.id) || readModConfig()?.workspaceId;
38
+ if (!effectiveWorkspaceId) {
39
+ set({ error: 'No workspace selected or configured (.mod/config.json)', loading: false });
40
+ return;
41
+ }
42
+ const branchContext = new BranchableRepo(repo);
43
+ // Resolve branchId for consistent branch-aware view (prefer config > workspace.activeBranchId > selectedThread)
44
+ let resolvedBranchId = undefined;
45
+ try {
46
+ resolvedBranchId = readModConfig()?.activeBranchId;
47
+ if (!resolvedBranchId) {
48
+ const wsHandle = await repo.find(effectiveWorkspaceId);
49
+ const wsDoc = await wsHandle.doc();
50
+ resolvedBranchId = wsDoc?.activeBranchId;
51
+ }
52
+ if (!resolvedBranchId && selectedThread?.branchId) {
53
+ resolvedBranchId = selectedThread.branchId;
54
+ }
55
+ }
56
+ catch { }
57
+ const wsWrap = await branchContext.openHandle(effectiveWorkspaceId, { branchId: (resolvedBranchId || undefined), workspaceId: effectiveWorkspaceId });
58
+ const wsDoc = wsWrap.doc();
59
+ const fileRefs = wsDoc.fileRefs || [];
60
+ const folders = wsDoc.folders || [];
61
+ // Build root-level items and dedupe by name to avoid duplicates
62
+ const itemMapByName = {};
63
+ for (const f of folders) {
64
+ if (!f.parentId) {
65
+ const item = { id: f.id, name: f.name, type: 'folder', parentId: f.parentId || null };
66
+ if (!itemMapByName[item.name])
67
+ itemMapByName[item.name] = item;
68
+ }
69
+ }
70
+ for (const f of fileRefs) {
71
+ if (!f.folderId) {
72
+ const item = { id: f.id, name: f.name, type: 'file', parentId: f.folderId || null };
73
+ if (!itemMapByName[item.name])
74
+ itemMapByName[item.name] = item;
75
+ }
76
+ }
77
+ const items = Object.values(itemMapByName).sort((a, b) => a.name.localeCompare(b.name));
78
+ const rootIds = items.map(i => i.id);
79
+ get().setItems(items, rootIds);
80
+ }
81
+ catch (err) {
82
+ set({ error: err.message || 'Failed to load directory' });
83
+ }
84
+ finally {
85
+ set({ loading: false });
86
+ }
87
+ },
88
+ }));
89
+ // React hook for using the vanilla store in components
90
+ export const useDirectoryStore = (selector) => useStore(directoryStore, selector);
91
+ // Custom hook for loading directory (merged from use-directory)
92
+ export function useDirectoryLoader() {
93
+ const loadRootDirectory = useDirectoryStore((s) => s.loadRootDirectory);
94
+ const reset = useDirectoryStore((s) => s.reset);
95
+ return { loadRootDirectory, reset };
96
+ }
@@ -0,0 +1,46 @@
1
+ import { create } from 'zustand';
2
+ import { BranchService } from '@mod/mod-core/services/branch-service';
3
+ export const useThreadsStore = create((set) => ({
4
+ threads: [],
5
+ loading: false,
6
+ error: null,
7
+ fetchThreads: async (repo, selected) => {
8
+ if (!selected || !repo)
9
+ return;
10
+ set({ loading: true, error: null });
11
+ try {
12
+ const wsHandle = await repo.find(selected.id);
13
+ const wsDoc = wsHandle.doc();
14
+ const threadList = [];
15
+ if (wsDoc?.branchesDocId) {
16
+ const branchService = new BranchService(repo);
17
+ const branches = await branchService.getBranchesForWorkspace(wsDoc.branchesDocId);
18
+ for (const branch of branches || []) {
19
+ const branchId = String(branch?.id || '');
20
+ if (!branchId)
21
+ continue;
22
+ const threadId = branch?.threadId ? String(branch.threadId) : branchId;
23
+ let threadDoc = null;
24
+ try {
25
+ const tHandle = await repo.find(threadId);
26
+ threadDoc = tHandle.doc();
27
+ }
28
+ catch { }
29
+ threadList.push({
30
+ id: String(threadDoc?.id || threadId),
31
+ name: threadDoc?.name || branch?.name || 'Untitled',
32
+ branchId,
33
+ hasThread: Boolean(branch?.threadId),
34
+ });
35
+ }
36
+ }
37
+ set({ threads: threadList });
38
+ }
39
+ catch (err) {
40
+ set({ error: err.message || String(err) });
41
+ }
42
+ finally {
43
+ set({ loading: false });
44
+ }
45
+ },
46
+ }));
@@ -0,0 +1,32 @@
1
+ import { create } from 'zustand';
2
+ export const useWorkspacesStore = create((set) => ({
3
+ workspaces: [],
4
+ loading: false,
5
+ error: null,
6
+ fetchWorkspaces: async (repo) => {
7
+ set({ loading: true, error: null });
8
+ try {
9
+ // const rootDocId = '3RrsuQCaieHkXPUNSsq3UhEAxnHW';
10
+ // const rootDocId = 'rY75AHsEfNx1vKnrtpdfmo5dyjr';
11
+ // const rootDocId = '2a9anXJNoBxstgrhRJfkBGS3uxs';
12
+ // const rootDocId = '3pW2EWyQPrshmj4Y1KqEo7sMTqxV';
13
+ const rootDocId = 'KRkJrMQeNQo9bTwSsoKZBxcpz1y';
14
+ const wsHandle = await repo.find(rootDocId);
15
+ const doc = wsHandle.doc();
16
+ const workspacesRaw = doc.workspaces || [];
17
+ const workspacesList = (Array.isArray(workspacesRaw)
18
+ ? workspacesRaw
19
+ : []).map((w) => ({
20
+ id: w.id || '',
21
+ name: w.name || w.title || 'Untitled',
22
+ }));
23
+ set({ workspaces: workspacesList });
24
+ }
25
+ catch (err) {
26
+ set({ error: err.message || String(err) });
27
+ }
28
+ finally {
29
+ set({ loading: false });
30
+ }
31
+ },
32
+ }));
@@ -0,0 +1,16 @@
1
+ // glassware[type=implementation, id=cli-config-types, requirements=req-cli-storage-config-1,req-cli-storage-config-2]
2
+ /**
3
+ * Default settings values.
4
+ */
5
+ export const DEFAULT_SETTINGS = {
6
+ syncDebounceMs: 100,
7
+ logLevel: 'info',
8
+ };
9
+ /**
10
+ * Default config for new installations.
11
+ */
12
+ export const DEFAULT_CONFIG = {
13
+ version: 1,
14
+ auth: null,
15
+ settings: DEFAULT_SETTINGS,
16
+ };
@@ -0,0 +1,2 @@
1
+ export * from './config.js';
2
+ export * from './workspace-connection.js';
@@ -0,0 +1,2 @@
1
+ // glassware[type=implementation, id=cli-workspace-connection-types, requirements=req-cli-storage-conn-1,req-cli-storage-conn-3]
2
+ export {};
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "@mod-computer/cli",
3
+ "version": "0.1.0",
4
+ "license": "MIT",
5
+ "bin": {
6
+ "mod": "dist/cli.bundle.js"
7
+ },
8
+ "type": "module",
9
+ "engines": {
10
+ "node": ">=16"
11
+ },
12
+ "scripts": {
13
+ "build": "tsc && node fix-imports.js && cp -r source/config/release-profiles dist/config/",
14
+ "build:bundle": "node build-bundle.js",
15
+ "prepublishOnly": "pnpm build:bundle",
16
+ "dev": "tsc --watch",
17
+ "test": "vitest run",
18
+ "package": "node build-for-docs.js"
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "commands"
23
+ ],
24
+ "dependencies": {
25
+ "@automerge/automerge": "^3.1.2",
26
+ "@automerge/automerge-repo": "^2.3.1",
27
+ "@automerge/automerge-repo-network-websocket": "^2.3.1",
28
+ "@automerge/automerge-repo-storage-nodefs": "^2.3.1",
29
+ "@ai-sdk/anthropic": "2.0.0-beta.3",
30
+ "@ai-sdk/openai": "2.0.0-beta.5",
31
+ "ai": "5.0.0-beta.11",
32
+ "chokidar": "^4.0.3",
33
+ "dotenv": "^17.1.0",
34
+ "ink": "^6.0.1",
35
+ "ink-select-input": "^6.2.0",
36
+ "ink-text-input": "^6.0.0",
37
+ "meow": "^11.0.0",
38
+ "react": "19.1.0",
39
+ "react-dom": "19.1.0",
40
+ "zustand": "^5.0.6"
41
+ },
42
+ "devDependencies": {
43
+ "@mod/mod-core": "workspace:*",
44
+ "esbuild": "^0.24.0",
45
+ "@babel/cli": "^7.21.0",
46
+ "@babel/preset-react": "^7.18.6",
47
+ "@types/ink": "^2.0.3",
48
+ "@types/ink-select-input": "^3.0.5",
49
+ "@types/ink-text-input": "^2.0.5",
50
+ "@types/meow": "^5.0.0",
51
+ "@types/node": "^24.0.10",
52
+ "@types/react": "^19.1.8",
53
+ "@vdemedes/prettier-config": "^2.0.1",
54
+ "chalk": "^5.2.0",
55
+ "eslint-config-xo-react": "^0.27.0",
56
+ "eslint-plugin-react": "^7.32.2",
57
+ "eslint-plugin-react-hooks": "^4.6.0",
58
+ "import-jsx": "^5.0.0",
59
+ "ink-testing-library": "^3.0.0",
60
+ "prettier": "^2.8.7",
61
+ "react-test-renderer": "^19.1.0",
62
+ "ts-node": "^10.9.2",
63
+ "typescript": "^5.8.3",
64
+ "vitest": "^1.6.1"
65
+ },
66
+ "prettier": "@vdemedes/prettier-config"
67
+ }