@tracemarketplace/cli 0.0.13 → 0.0.17

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 (94) hide show
  1. package/dist/api-client.d.ts +9 -2
  2. package/dist/api-client.d.ts.map +1 -1
  3. package/dist/api-client.js +80 -15
  4. package/dist/api-client.js.map +1 -1
  5. package/dist/api-client.test.d.ts +2 -0
  6. package/dist/api-client.test.d.ts.map +1 -0
  7. package/dist/api-client.test.js +34 -0
  8. package/dist/api-client.test.js.map +1 -0
  9. package/dist/cli.js +48 -18
  10. package/dist/cli.js.map +1 -1
  11. package/dist/commands/auto-submit.d.ts +2 -1
  12. package/dist/commands/auto-submit.d.ts.map +1 -1
  13. package/dist/commands/auto-submit.js +43 -56
  14. package/dist/commands/auto-submit.js.map +1 -1
  15. package/dist/commands/daemon.d.ts +8 -1
  16. package/dist/commands/daemon.d.ts.map +1 -1
  17. package/dist/commands/daemon.js +184 -63
  18. package/dist/commands/daemon.js.map +1 -1
  19. package/dist/commands/history.d.ts +3 -1
  20. package/dist/commands/history.d.ts.map +1 -1
  21. package/dist/commands/history.js +8 -4
  22. package/dist/commands/history.js.map +1 -1
  23. package/dist/commands/login.d.ts +5 -1
  24. package/dist/commands/login.d.ts.map +1 -1
  25. package/dist/commands/login.js +25 -9
  26. package/dist/commands/login.js.map +1 -1
  27. package/dist/commands/register.d.ts +1 -0
  28. package/dist/commands/register.d.ts.map +1 -1
  29. package/dist/commands/register.js +4 -39
  30. package/dist/commands/register.js.map +1 -1
  31. package/dist/commands/remove-daemon.d.ts +6 -0
  32. package/dist/commands/remove-daemon.d.ts.map +1 -0
  33. package/dist/commands/remove-daemon.js +66 -0
  34. package/dist/commands/remove-daemon.js.map +1 -0
  35. package/dist/commands/remove-hook.d.ts +6 -0
  36. package/dist/commands/remove-hook.d.ts.map +1 -0
  37. package/dist/commands/remove-hook.js +174 -0
  38. package/dist/commands/remove-hook.js.map +1 -0
  39. package/dist/commands/setup-hook.d.ts +2 -0
  40. package/dist/commands/setup-hook.d.ts.map +1 -1
  41. package/dist/commands/setup-hook.js +85 -41
  42. package/dist/commands/setup-hook.js.map +1 -1
  43. package/dist/commands/status.d.ts +3 -1
  44. package/dist/commands/status.d.ts.map +1 -1
  45. package/dist/commands/status.js +8 -4
  46. package/dist/commands/status.js.map +1 -1
  47. package/dist/commands/submit.d.ts +1 -0
  48. package/dist/commands/submit.d.ts.map +1 -1
  49. package/dist/commands/submit.js +138 -83
  50. package/dist/commands/submit.js.map +1 -1
  51. package/dist/commands/whoami.d.ts +3 -1
  52. package/dist/commands/whoami.d.ts.map +1 -1
  53. package/dist/commands/whoami.js +8 -4
  54. package/dist/commands/whoami.js.map +1 -1
  55. package/dist/config.d.ts +38 -6
  56. package/dist/config.d.ts.map +1 -1
  57. package/dist/config.js +175 -17
  58. package/dist/config.js.map +1 -1
  59. package/dist/constants.d.ts +8 -0
  60. package/dist/constants.d.ts.map +1 -0
  61. package/dist/constants.js +16 -0
  62. package/dist/constants.js.map +1 -0
  63. package/dist/flush.d.ts +49 -0
  64. package/dist/flush.d.ts.map +1 -0
  65. package/dist/flush.js +405 -0
  66. package/dist/flush.js.map +1 -0
  67. package/dist/flush.test.d.ts +2 -0
  68. package/dist/flush.test.d.ts.map +1 -0
  69. package/dist/flush.test.js +330 -0
  70. package/dist/flush.test.js.map +1 -0
  71. package/dist/submitter.d.ts.map +1 -1
  72. package/dist/submitter.js +5 -2
  73. package/dist/submitter.js.map +1 -1
  74. package/package.json +8 -7
  75. package/src/api-client.test.ts +47 -0
  76. package/src/api-client.ts +100 -16
  77. package/src/cli.ts +55 -19
  78. package/src/commands/auto-submit.ts +80 -40
  79. package/src/commands/daemon.ts +243 -60
  80. package/src/commands/history.ts +9 -4
  81. package/src/commands/login.ts +37 -9
  82. package/src/commands/remove-daemon.ts +75 -0
  83. package/src/commands/remove-hook.ts +194 -0
  84. package/src/commands/setup-hook.ts +93 -43
  85. package/src/commands/status.ts +8 -4
  86. package/src/commands/submit.ts +191 -83
  87. package/src/commands/whoami.ts +8 -4
  88. package/src/config.ts +241 -21
  89. package/src/constants.ts +18 -0
  90. package/src/flush.test.ts +395 -0
  91. package/src/flush.ts +591 -0
  92. package/vitest.config.ts +8 -0
  93. package/src/commands/register.ts +0 -52
  94. package/src/submitter.ts +0 -110
package/src/config.ts CHANGED
@@ -1,19 +1,49 @@
1
1
  import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
2
  import { homedir } from "os";
3
3
  import { join } from "path";
4
+ import { DEFAULT_PROFILE } from "./constants.js";
4
5
 
5
- export interface Config {
6
+ export interface StoredProfileConfig {
6
7
  apiKey: string;
7
8
  serverUrl: string;
8
9
  email: string;
9
10
  }
10
11
 
11
- // Tracks which chunks of each session have been submitted.
12
- // Key: "<source_tool>:<source_session_id>", value: highest chunk_index submitted.
12
+ export interface Config extends StoredProfileConfig {
13
+ profile: string;
14
+ }
15
+
16
+ export interface ConfigStore {
17
+ defaultProfile: string;
18
+ profiles: Record<string, StoredProfileConfig>;
19
+ }
20
+
21
+ export type TrackedSessionTool = "claude_code" | "codex_cli" | "cursor";
22
+
23
+ export interface SessionUploadState {
24
+ sourceTool: TrackedSessionTool;
25
+ sourceSessionId: string;
26
+ locator: string;
27
+ nextChunkIndex: number;
28
+ openChunkStartTurn: number;
29
+ lastSeenTurnCount: number;
30
+ lastActivityAt: string | null;
31
+ lastFlushedTurnId: string | null;
32
+ // Async confirmation tracking
33
+ confirmedChunkIndex: number; // all chunks below this index are confirmed in DB
34
+ confirmedOpenChunkStartTurn: number; // openChunkStartTurn to restore if re-submitting
35
+ unconfirmedSince: string | null; // ISO timestamp when chunks first went unconfirmed
36
+ }
37
+
13
38
  export interface SubmitState {
39
+ version: 2;
14
40
  chunks: Record<string, number>;
41
+ sessions: Record<string, SessionUploadState>;
15
42
  }
16
43
 
44
+ type JsonRecord = Record<string, unknown>;
45
+ const EMPTY_SUBMIT_STATE: SubmitState = { version: 2, chunks: {}, sessions: {} };
46
+
17
47
  export function getConfigDir(): string {
18
48
  return join(homedir(), ".config", "tracemarketplace");
19
49
  }
@@ -22,40 +52,230 @@ export function getConfigPath(): string {
22
52
  return join(getConfigDir(), "config.json");
23
53
  }
24
54
 
25
- export function getStatePath(): string {
26
- return join(getConfigDir(), "state.json");
55
+ export function getStatePath(profile?: string): string {
56
+ return join(getConfigDir(), `state${profileSuffix(profile)}.json`);
27
57
  }
28
58
 
29
- export function loadConfig(): Config | null {
30
- const p = getConfigPath();
31
- if (!existsSync(p)) return null;
59
+ export function getAutoSubmitLogPath(profile?: string): string {
60
+ return join(getConfigDir(), `auto-submit${profileSuffix(profile)}.log`);
61
+ }
62
+
63
+ export function getDaemonStatePath(profile?: string): string {
64
+ return join(getConfigDir(), `daemon-state${profileSuffix(profile)}.json`);
65
+ }
66
+
67
+ export function getDaemonPidPath(profile?: string): string {
68
+ return join(getConfigDir(), `daemon${profileSuffix(profile)}.pid`);
69
+ }
70
+
71
+ export function resolveProfile(profile?: string): string {
72
+ if (profile) return normalizeProfile(profile);
73
+ if (process.env.TRACEMP_PROFILE) return normalizeProfile(process.env.TRACEMP_PROFILE);
74
+ return normalizeProfile(loadConfigStore().defaultProfile);
75
+ }
76
+
77
+ export function loadConfigStore(): ConfigStore {
78
+ const raw = readConfigFile();
79
+ if (isStoredProfileConfig(raw)) {
80
+ return {
81
+ defaultProfile: DEFAULT_PROFILE,
82
+ profiles: { [DEFAULT_PROFILE]: normalizeStoredProfileConfig(raw) },
83
+ };
84
+ }
85
+
86
+ if (!isRecord(raw)) {
87
+ return {
88
+ defaultProfile: DEFAULT_PROFILE,
89
+ profiles: {},
90
+ };
91
+ }
92
+
93
+ const profiles = normalizeProfiles(raw.profiles);
94
+ const requestedDefault = normalizeProfile(
95
+ typeof raw.defaultProfile === "string" ? raw.defaultProfile : DEFAULT_PROFILE
96
+ );
97
+
98
+ return {
99
+ defaultProfile: profiles[requestedDefault]
100
+ ? requestedDefault
101
+ : profiles[DEFAULT_PROFILE]
102
+ ? DEFAULT_PROFILE
103
+ : requestedDefault,
104
+ profiles,
105
+ };
106
+ }
107
+
108
+ export function loadConfig(profile?: string): Config | null {
109
+ const store = loadConfigStore();
110
+ const resolvedProfile = resolveProfileFromStore(store, profile);
111
+ const selected = store.profiles[resolvedProfile];
112
+ if (!selected) return null;
113
+ return { profile: resolvedProfile, ...selected };
114
+ }
115
+
116
+ export function saveConfig(
117
+ config: StoredProfileConfig,
118
+ options: { profile?: string; setDefault?: boolean } = {}
119
+ ): Config {
120
+ const store = loadConfigStore();
121
+ const profile = normalizeProfile(options.profile);
122
+
123
+ store.profiles[profile] = normalizeStoredProfileConfig(config);
124
+
125
+ const normalizedDefault = normalizeProfile(store.defaultProfile);
126
+ if (options.setDefault || !store.profiles[normalizedDefault]) {
127
+ store.defaultProfile = profile;
128
+ } else {
129
+ store.defaultProfile = normalizedDefault;
130
+ }
131
+
132
+ writeConfigStore(store);
133
+ return { profile, ...store.profiles[profile] };
134
+ }
135
+
136
+ export function loadState(profile?: string): SubmitState {
137
+ const p = getStatePath(profile);
138
+ if (!existsSync(p)) return EMPTY_SUBMIT_STATE;
32
139
  try {
33
- return JSON.parse(readFileSync(p, "utf-8")) as Config;
140
+ return normalizeSubmitState(JSON.parse(readFileSync(p, "utf-8")));
34
141
  } catch {
35
- return null;
142
+ return EMPTY_SUBMIT_STATE;
36
143
  }
37
144
  }
38
145
 
39
- export function saveConfig(config: Config): void {
146
+ export function saveState(state: SubmitState, profile?: string): void {
40
147
  mkdirSync(getConfigDir(), { recursive: true });
41
- writeFileSync(getConfigPath(), JSON.stringify(config, null, 2), "utf-8");
148
+ writeFileSync(getStatePath(profile), JSON.stringify(state, null, 2) + "\n", "utf-8");
149
+ }
150
+
151
+ export function stateKey(sourceTool: string, sessionId: string): string {
152
+ return `${sourceTool}:${sessionId}`;
42
153
  }
43
154
 
44
- export function loadState(): SubmitState {
45
- const p = getStatePath();
46
- if (!existsSync(p)) return { chunks: {} };
155
+ function readConfigFile(): unknown {
156
+ const p = getConfigPath();
157
+ if (!existsSync(p)) return null;
47
158
  try {
48
- return JSON.parse(readFileSync(p, "utf-8")) as SubmitState;
159
+ return JSON.parse(readFileSync(p, "utf-8"));
49
160
  } catch {
50
- return { chunks: {} };
161
+ return null;
51
162
  }
52
163
  }
53
164
 
54
- export function saveState(state: SubmitState): void {
165
+ function writeConfigStore(store: ConfigStore): void {
166
+ const profiles = normalizeProfiles(store.profiles);
167
+ const requestedDefault = normalizeProfile(store.defaultProfile);
168
+ const defaultProfile = profiles[requestedDefault]
169
+ ? requestedDefault
170
+ : profiles[DEFAULT_PROFILE]
171
+ ? DEFAULT_PROFILE
172
+ : requestedDefault;
173
+
55
174
  mkdirSync(getConfigDir(), { recursive: true });
56
- writeFileSync(getStatePath(), JSON.stringify(state, null, 2), "utf-8");
175
+ writeFileSync(
176
+ getConfigPath(),
177
+ JSON.stringify({ defaultProfile, profiles }, null, 2) + "\n",
178
+ "utf-8"
179
+ );
57
180
  }
58
181
 
59
- export function stateKey(sourceTool: string, sessionId: string): string {
60
- return `${sourceTool}:${sessionId}`;
182
+ function resolveProfileFromStore(store: ConfigStore, profile?: string): string {
183
+ if (profile) return normalizeProfile(profile);
184
+ if (process.env.TRACEMP_PROFILE) return normalizeProfile(process.env.TRACEMP_PROFILE);
185
+ return normalizeProfile(store.defaultProfile);
186
+ }
187
+
188
+ function profileSuffix(profile?: string): string {
189
+ const resolvedProfile = normalizeProfile(profile);
190
+ return resolvedProfile === DEFAULT_PROFILE ? "" : `.${resolvedProfile}`;
191
+ }
192
+
193
+ function normalizeProfile(profile?: string | null): string {
194
+ const trimmed = (profile ?? "").trim().toLowerCase();
195
+ if (!trimmed) return DEFAULT_PROFILE;
196
+
197
+ const normalized = trimmed
198
+ .replace(/[^a-z0-9_-]+/g, "-")
199
+ .replace(/^-+/, "")
200
+ .replace(/-+$/, "");
201
+
202
+ return normalized || DEFAULT_PROFILE;
203
+ }
204
+
205
+ function normalizeProfiles(value: unknown): Record<string, StoredProfileConfig> {
206
+ if (!isRecord(value)) return {};
207
+
208
+ const profiles: Record<string, StoredProfileConfig> = {};
209
+ for (const [profile, config] of Object.entries(value)) {
210
+ if (!isStoredProfileConfig(config)) continue;
211
+ profiles[normalizeProfile(profile)] = normalizeStoredProfileConfig(config);
212
+ }
213
+ return profiles;
214
+ }
215
+
216
+ function normalizeStoredProfileConfig(config: StoredProfileConfig): StoredProfileConfig {
217
+ return {
218
+ apiKey: config.apiKey,
219
+ serverUrl: config.serverUrl,
220
+ email: config.email,
221
+ };
222
+ }
223
+
224
+ function normalizeSubmitState(value: unknown): SubmitState {
225
+ if (!isRecord(value)) {
226
+ return EMPTY_SUBMIT_STATE;
227
+ }
228
+
229
+ const chunks = isRecord(value.chunks)
230
+ ? Object.fromEntries(
231
+ Object.entries(value.chunks)
232
+ .filter((entry): entry is [string, number] => typeof entry[1] === "number")
233
+ )
234
+ : {};
235
+
236
+ const sessions = isRecord(value.sessions)
237
+ ? Object.fromEntries(
238
+ Object.entries(value.sessions)
239
+ .filter((entry): entry is [string, SessionUploadState] => isSessionUploadState(entry[1]))
240
+ )
241
+ : {};
242
+
243
+ return {
244
+ version: 2,
245
+ chunks,
246
+ sessions,
247
+ };
248
+ }
249
+
250
+ function isRecord(value: unknown): value is JsonRecord {
251
+ return typeof value === "object" && value !== null && !Array.isArray(value);
252
+ }
253
+
254
+ function isStoredProfileConfig(value: unknown): value is StoredProfileConfig {
255
+ return isRecord(value)
256
+ && typeof value.apiKey === "string"
257
+ && typeof value.serverUrl === "string"
258
+ && typeof value.email === "string";
259
+ }
260
+
261
+ function isSessionUploadState(value: unknown): value is SessionUploadState {
262
+ return isRecord(value)
263
+ && (value.sourceTool === "claude_code" || value.sourceTool === "codex_cli" || value.sourceTool === "cursor")
264
+ && typeof value.sourceSessionId === "string"
265
+ && typeof value.locator === "string"
266
+ && typeof value.nextChunkIndex === "number"
267
+ && typeof value.openChunkStartTurn === "number"
268
+ && typeof value.lastSeenTurnCount === "number"
269
+ && (value.lastActivityAt === null || typeof value.lastActivityAt === "string")
270
+ && (value.lastFlushedTurnId === null || typeof value.lastFlushedTurnId === "string");
271
+ }
272
+
273
+ export function migrateSessionUploadState(value: SessionUploadState): SessionUploadState {
274
+ return {
275
+ ...value,
276
+ // Backward compat: existing sessions assume all submitted chunks are confirmed
277
+ confirmedChunkIndex: value.confirmedChunkIndex ?? value.nextChunkIndex,
278
+ confirmedOpenChunkStartTurn: value.confirmedOpenChunkStartTurn ?? value.openChunkStartTurn,
279
+ unconfirmedSince: value.unconfirmedSince ?? null,
280
+ };
61
281
  }
@@ -0,0 +1,18 @@
1
+ export const CLI_NAME = "tracemp";
2
+ export const DEFAULT_PROFILE = "prod";
3
+ export const PROD_SERVER_URL = "https://trace-marketplace-api.fly.dev";
4
+ export const DEV_SERVER_URL = "http://localhost:3001";
5
+
6
+ export function defaultServerUrlForProfile(profile: string): string {
7
+ return profile === "dev" ? DEV_SERVER_URL : PROD_SERVER_URL;
8
+ }
9
+
10
+ export function inferProfileFromServerUrl(serverUrl: string): string {
11
+ return serverUrl === PROD_SERVER_URL ? DEFAULT_PROFILE : "dev";
12
+ }
13
+
14
+ export function loginCommandForProfile(profile: string): string {
15
+ return profile === DEFAULT_PROFILE
16
+ ? `${CLI_NAME} login`
17
+ : `${CLI_NAME} login --profile ${profile}`;
18
+ }