@residue/cli 0.0.2 → 0.0.4

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/dist/index.js CHANGED
@@ -3622,10 +3622,40 @@ function capture() {
3622
3622
  });
3623
3623
  }
3624
3624
 
3625
+ // src/commands/clear.ts
3626
+ var log2 = createLogger("clear");
3627
+ function clear(opts) {
3628
+ return safeTry(async function* () {
3629
+ const projectRoot = yield* getProjectRoot();
3630
+ const pendingPath = yield* getPendingPath(projectRoot);
3631
+ const sessions = yield* readPending(pendingPath);
3632
+ if (sessions.length === 0) {
3633
+ log2.info("No pending sessions to clear.");
3634
+ return ok(undefined);
3635
+ }
3636
+ if (opts?.id) {
3637
+ const targetId = opts.id;
3638
+ const isFound = sessions.some((s) => s.id === targetId);
3639
+ if (!isFound) {
3640
+ log2.info(`Session ${targetId} not found in pending queue.`);
3641
+ return ok(undefined);
3642
+ }
3643
+ const remaining = sessions.filter((s) => s.id !== targetId);
3644
+ yield* writePending({ path: pendingPath, sessions: remaining });
3645
+ log2.info(`Cleared session ${targetId}.`);
3646
+ return ok(undefined);
3647
+ }
3648
+ const count = sessions.length;
3649
+ yield* writePending({ path: pendingPath, sessions: [] });
3650
+ log2.info(`Cleared ${count} pending session(s).`);
3651
+ return ok(undefined);
3652
+ });
3653
+ }
3654
+
3625
3655
  // src/commands/hook.ts
3626
3656
  import { mkdir as mkdir2, readFile, rm, stat, writeFile } from "fs/promises";
3627
3657
  import { join as join2 } from "path";
3628
- var log2 = createLogger("hook");
3658
+ var log3 = createLogger("hook");
3629
3659
  function readStdin() {
3630
3660
  return ResultAsync.fromPromise((async () => {
3631
3661
  const chunks = [];
@@ -3722,7 +3752,7 @@ function handleSessionStart(opts) {
3722
3752
  message: "Failed to write hook state file",
3723
3753
  code: "IO_ERROR"
3724
3754
  }));
3725
- log2.debug("session started for claude-code");
3755
+ log3.debug("session started for claude-code");
3726
3756
  return ok(undefined);
3727
3757
  });
3728
3758
  }
@@ -3754,7 +3784,7 @@ function handleSessionEnd(opts) {
3754
3784
  message: "Failed to remove hook state file",
3755
3785
  code: "IO_ERROR"
3756
3786
  }));
3757
- log2.debug("session %s ended", trimmedId);
3787
+ log3.debug("session %s ended", trimmedId);
3758
3788
  return ok(undefined);
3759
3789
  });
3760
3790
  }
@@ -3785,7 +3815,7 @@ import {
3785
3815
  writeFile as writeFile2
3786
3816
  } from "fs/promises";
3787
3817
  import { join as join3 } from "path";
3788
- var log3 = createLogger("init");
3818
+ var log4 = createLogger("init");
3789
3819
  var POST_COMMIT_LINE = "residue capture >/dev/null 2>&1 &";
3790
3820
  var PRE_PUSH_LINE = 'residue sync --remote-url "$2"';
3791
3821
  function installHook(opts) {
@@ -3876,9 +3906,9 @@ function init() {
3876
3906
  }),
3877
3907
  ensureGitignore(projectRoot)
3878
3908
  ]);
3879
- log3.info("Initialized residue in this repository.");
3880
- log3.info(` ${postCommit}`);
3881
- log3.info(` ${prePush}`);
3909
+ log4.info("Initialized residue in this repository.");
3910
+ log4.info(` ${postCommit}`);
3911
+ log4.info(` ${prePush}`);
3882
3912
  return ok(undefined);
3883
3913
  });
3884
3914
  }
@@ -3937,7 +3967,7 @@ function resolveConfig() {
3937
3967
  }
3938
3968
 
3939
3969
  // src/commands/login.ts
3940
- var log4 = createLogger("login");
3970
+ var log5 = createLogger("login");
3941
3971
  function login(opts) {
3942
3972
  if (!opts.url.startsWith("http://") && !opts.url.startsWith("https://")) {
3943
3973
  return errAsync(new CliError({
@@ -3949,17 +3979,164 @@ function login(opts) {
3949
3979
  const config = { worker_url: cleanUrl, token: opts.token };
3950
3980
  if (opts.isLocal) {
3951
3981
  return getProjectRoot().andThen((projectRoot) => writeLocalConfig({ projectRoot, config }).map(() => {
3952
- log4.info(`Logged in to ${cleanUrl} (project-local config)`);
3982
+ log5.info(`Logged in to ${cleanUrl} (project-local config)`);
3953
3983
  }));
3954
3984
  }
3955
3985
  return writeConfig(config).map(() => {
3956
- log4.info(`Logged in to ${cleanUrl}`);
3986
+ log5.info(`Logged in to ${cleanUrl}`);
3957
3987
  });
3958
3988
  }
3959
3989
 
3990
+ // src/lib/search-text.ts
3991
+ function buildSearchText(opts) {
3992
+ const header = [
3993
+ `Session: ${opts.metadata.sessionId}`,
3994
+ `Agent: ${opts.metadata.agent}`,
3995
+ opts.metadata.commits.length > 0 ? `Commits: ${opts.metadata.commits.join(", ")}` : null,
3996
+ opts.metadata.branch ? `Branch: ${opts.metadata.branch}` : null,
3997
+ opts.metadata.repo ? `Repo: ${opts.metadata.repo}` : null
3998
+ ].filter(Boolean).join(`
3999
+ `);
4000
+ const body = opts.lines.map((line) => `[${line.role}] ${line.text}`).join(`
4001
+ `);
4002
+ return `${header}
4003
+
4004
+ ${body}
4005
+ `;
4006
+ }
4007
+ function extractClaudeCode(raw) {
4008
+ const lines = [];
4009
+ if (!raw.trim())
4010
+ return lines;
4011
+ const entries = [];
4012
+ for (const line of raw.split(`
4013
+ `)) {
4014
+ if (!line.trim())
4015
+ continue;
4016
+ try {
4017
+ entries.push(JSON.parse(line));
4018
+ } catch {}
4019
+ }
4020
+ for (const entry of entries) {
4021
+ if (entry.isMeta || entry.isSidechain)
4022
+ continue;
4023
+ if (entry.type === "user") {
4024
+ const content = entry.message?.content;
4025
+ if (!content)
4026
+ continue;
4027
+ if (typeof content === "string") {
4028
+ const trimmed = content.trim();
4029
+ if (trimmed)
4030
+ lines.push({ role: "human", text: trimmed });
4031
+ } else if (Array.isArray(content)) {
4032
+ const hasToolResult = content.some((b) => b.type === "tool_result");
4033
+ if (hasToolResult)
4034
+ continue;
4035
+ const text = content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join(`
4036
+ `).trim();
4037
+ if (text)
4038
+ lines.push({ role: "human", text });
4039
+ }
4040
+ } else if (entry.type === "assistant") {
4041
+ const content = entry.message?.content;
4042
+ if (!Array.isArray(content))
4043
+ continue;
4044
+ for (const block of content) {
4045
+ if (block.type === "text" && block.text) {
4046
+ const trimmed = block.text.trim();
4047
+ if (trimmed)
4048
+ lines.push({ role: "assistant", text: trimmed });
4049
+ } else if (block.type === "tool_use" && block.name) {
4050
+ const desc = summarizeToolInput(block.name, block.input);
4051
+ lines.push({ role: "tool", text: desc });
4052
+ }
4053
+ }
4054
+ }
4055
+ }
4056
+ return lines;
4057
+ }
4058
+ function extractPi(raw) {
4059
+ const lines = [];
4060
+ if (!raw.trim())
4061
+ return lines;
4062
+ const entries = [];
4063
+ for (const line of raw.split(`
4064
+ `)) {
4065
+ if (!line.trim())
4066
+ continue;
4067
+ try {
4068
+ entries.push(JSON.parse(line));
4069
+ } catch {}
4070
+ }
4071
+ for (const entry of entries) {
4072
+ if (entry.type !== "message")
4073
+ continue;
4074
+ const msg = entry.message;
4075
+ if (!msg)
4076
+ continue;
4077
+ if (msg.role === "user") {
4078
+ const content = msg.content;
4079
+ if (!content)
4080
+ continue;
4081
+ if (typeof content === "string") {
4082
+ const trimmed = content.trim();
4083
+ if (trimmed)
4084
+ lines.push({ role: "human", text: trimmed });
4085
+ } else if (Array.isArray(content)) {
4086
+ const text = content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join(`
4087
+ `).trim();
4088
+ if (text)
4089
+ lines.push({ role: "human", text });
4090
+ }
4091
+ } else if (msg.role === "assistant") {
4092
+ const content = msg.content;
4093
+ if (!Array.isArray(content)) {
4094
+ if (typeof content === "string" && content.trim()) {
4095
+ lines.push({ role: "assistant", text: content.trim() });
4096
+ }
4097
+ continue;
4098
+ }
4099
+ for (const block of content) {
4100
+ if (block.type === "text" && block.text) {
4101
+ const trimmed = block.text.trim();
4102
+ if (trimmed)
4103
+ lines.push({ role: "assistant", text: trimmed });
4104
+ } else if (block.type === "toolCall" && block.name) {
4105
+ const desc = summarizeToolInput(block.name, block.arguments);
4106
+ lines.push({ role: "tool", text: desc });
4107
+ }
4108
+ }
4109
+ }
4110
+ }
4111
+ return lines;
4112
+ }
4113
+ function summarizeToolInput(name, input) {
4114
+ if (!input)
4115
+ return name;
4116
+ const path = input.path ?? input.file_path ?? input.filePath ?? input.filename;
4117
+ if (typeof path === "string")
4118
+ return `${name} ${path}`;
4119
+ const command = input.command ?? input.cmd;
4120
+ if (typeof command === "string") {
4121
+ const short = command.length > 120 ? command.slice(0, 120) + "..." : command;
4122
+ return `${name} ${short}`;
4123
+ }
4124
+ const query = input.query ?? input.search ?? input.pattern;
4125
+ if (typeof query === "string")
4126
+ return `${name} ${query}`;
4127
+ return name;
4128
+ }
4129
+ var extractors = {
4130
+ "claude-code": extractClaudeCode,
4131
+ pi: extractPi
4132
+ };
4133
+ function getExtractor(agent) {
4134
+ return extractors[agent] ?? null;
4135
+ }
4136
+
3960
4137
  // src/commands/sync.ts
3961
4138
  import { stat as stat3 } from "fs/promises";
3962
- var log5 = createLogger("sync");
4139
+ var log6 = createLogger("sync");
3963
4140
  var STALE_THRESHOLD_MS = 30 * 60 * 1000;
3964
4141
  function requestUploadUrl(opts) {
3965
4142
  return ResultAsync.fromPromise(fetch(`${opts.workerUrl}/api/sessions/upload-url`, {
@@ -4022,7 +4199,7 @@ function buildCommitMeta(opts) {
4022
4199
  for (const ref of opts.commitRefs) {
4023
4200
  const metaResult = await getCommitMeta(ref.sha);
4024
4201
  if (metaResult.isErr()) {
4025
- log5.warn(metaResult.error);
4202
+ log6.warn(metaResult.error);
4026
4203
  continue;
4027
4204
  }
4028
4205
  commits.push({
@@ -4050,17 +4227,54 @@ function closeStaleOpenSessions(opts) {
4050
4227
  const checks = openSessions.map((session) => getFileMtimeMs(session.data_path).map((mtimeMs) => {
4051
4228
  if (mtimeMs === null) {
4052
4229
  session.status = "ended";
4053
- log5.debug("auto-closed session %s (data file not accessible)", session.id);
4230
+ log6.debug("auto-closed session %s (data file not accessible)", session.id);
4054
4231
  } else {
4055
4232
  const msSinceModified = now - mtimeMs;
4056
4233
  if (msSinceModified > STALE_THRESHOLD_MS) {
4057
4234
  session.status = "ended";
4058
- log5.debug("auto-closed stale session %s (data file unchanged for %dm)", session.id, Math.round(msSinceModified / 60000));
4235
+ log6.debug("auto-closed stale session %s (data file unchanged for %dm)", session.id, Math.round(msSinceModified / 60000));
4059
4236
  }
4060
4237
  }
4061
4238
  }));
4062
4239
  return ResultAsync.combine(checks).map(() => opts.sessions);
4063
4240
  }
4241
+ function generateSearchText(opts) {
4242
+ const extractor = getExtractor(opts.session.agent);
4243
+ if (!extractor) {
4244
+ log6.debug("no search text extractor for agent %s, skipping", opts.session.agent);
4245
+ return null;
4246
+ }
4247
+ const searchLines = extractor(opts.rawData);
4248
+ if (searchLines.length === 0)
4249
+ return null;
4250
+ const branches = [
4251
+ ...new Set(opts.session.commits.map((c) => c.branch).filter(Boolean))
4252
+ ];
4253
+ return buildSearchText({
4254
+ metadata: {
4255
+ sessionId: opts.session.id,
4256
+ agent: opts.session.agent,
4257
+ commits: opts.commits.map((c) => c.sha.slice(0, 7)),
4258
+ branch: branches[0] ?? "",
4259
+ repo: `${opts.org}/${opts.repo}`
4260
+ },
4261
+ lines: searchLines
4262
+ });
4263
+ }
4264
+ function uploadSearchText(opts) {
4265
+ return ResultAsync.fromPromise(fetch(opts.url, {
4266
+ method: "PUT",
4267
+ headers: { "Content-Type": "text/plain" },
4268
+ body: opts.data
4269
+ }).then((response) => {
4270
+ if (!response.ok) {
4271
+ throw new Error(`R2 search upload failed: HTTP ${response.status}`);
4272
+ }
4273
+ }), toCliError({
4274
+ message: "Search text R2 upload failed",
4275
+ code: "NETWORK_ERROR"
4276
+ }));
4277
+ }
4064
4278
  function syncSessions(opts) {
4065
4279
  return ResultAsync.fromSafePromise((async () => {
4066
4280
  const remaining = [];
@@ -4071,13 +4285,13 @@ function syncSessions(opts) {
4071
4285
  }
4072
4286
  const dataResult = await readSessionData(session.data_path);
4073
4287
  if (dataResult.isErr()) {
4074
- log5.warn(dataResult.error);
4288
+ log6.warn(dataResult.error);
4075
4289
  remaining.push(session);
4076
4290
  continue;
4077
4291
  }
4078
4292
  const data = dataResult.value;
4079
4293
  if (data === null) {
4080
- log5.warn(`dropping session ${session.id}: data file missing at ${session.data_path}`);
4294
+ log6.warn(`dropping session ${session.id}: data file missing at ${session.data_path}`);
4081
4295
  continue;
4082
4296
  }
4083
4297
  const commitsResult = await buildCommitMeta({
@@ -4086,7 +4300,7 @@ function syncSessions(opts) {
4086
4300
  repo: opts.repo
4087
4301
  });
4088
4302
  if (commitsResult.isErr()) {
4089
- log5.warn(commitsResult.error);
4303
+ log6.warn(commitsResult.error);
4090
4304
  remaining.push(session);
4091
4305
  continue;
4092
4306
  }
@@ -4096,7 +4310,7 @@ function syncSessions(opts) {
4096
4310
  sessionId: session.id
4097
4311
  });
4098
4312
  if (uploadUrlResult.isErr()) {
4099
- log5.warn(`failed to get upload URL for session ${session.id}: ${uploadUrlResult.error.message}`);
4313
+ log6.warn(`failed to get upload URL for session ${session.id}: ${uploadUrlResult.error.message}`);
4100
4314
  remaining.push(session);
4101
4315
  continue;
4102
4316
  }
@@ -4105,11 +4319,29 @@ function syncSessions(opts) {
4105
4319
  data
4106
4320
  });
4107
4321
  if (directUploadResult.isErr()) {
4108
- log5.warn(`R2 upload failed for session ${session.id}: ${directUploadResult.error.message}`);
4322
+ log6.warn(`R2 upload failed for session ${session.id}: ${directUploadResult.error.message}`);
4109
4323
  remaining.push(session);
4110
4324
  continue;
4111
4325
  }
4112
- log5.debug("uploaded session %s data directly to R2", session.id);
4326
+ log6.debug("uploaded session %s data directly to R2", session.id);
4327
+ const searchText = generateSearchText({
4328
+ session,
4329
+ rawData: data,
4330
+ commits: commitsResult.value,
4331
+ org: opts.org,
4332
+ repo: opts.repo
4333
+ });
4334
+ if (searchText && uploadUrlResult.value.search_url) {
4335
+ const searchUploadResult = await uploadSearchText({
4336
+ url: uploadUrlResult.value.search_url,
4337
+ data: searchText
4338
+ });
4339
+ if (searchUploadResult.isErr()) {
4340
+ log6.warn(`search text upload failed for session ${session.id}: ${searchUploadResult.error.message}`);
4341
+ } else {
4342
+ log6.debug("uploaded search text for session %s", session.id);
4343
+ }
4344
+ }
4113
4345
  const metadataResult = await postSessionMetadata({
4114
4346
  workerUrl: opts.workerUrl,
4115
4347
  token: opts.token,
@@ -4122,14 +4354,14 @@ function syncSessions(opts) {
4122
4354
  commits: commitsResult.value
4123
4355
  });
4124
4356
  if (metadataResult.isErr()) {
4125
- log5.warn(`metadata upload failed for session ${session.id}: ${metadataResult.error.message}`);
4357
+ log6.warn(`metadata upload failed for session ${session.id}: ${metadataResult.error.message}`);
4126
4358
  remaining.push(session);
4127
4359
  continue;
4128
4360
  }
4129
4361
  if (session.status === "open") {
4130
4362
  remaining.push(session);
4131
4363
  }
4132
- log5.debug("synced session %s", session.id);
4364
+ log6.debug("synced session %s", session.id);
4133
4365
  }
4134
4366
  return remaining;
4135
4367
  })());
@@ -4175,8 +4407,157 @@ function sync(opts) {
4175
4407
  // src/commands/push.ts
4176
4408
  var push = sync;
4177
4409
 
4410
+ // src/commands/search.ts
4411
+ var log7 = createLogger("search");
4412
+ function fetchSearch(opts) {
4413
+ const path = opts.isAi ? "/api/search/ai" : "/api/search";
4414
+ const url = `${opts.workerUrl}${path}?q=${encodeURIComponent(opts.query)}`;
4415
+ return ResultAsync.fromPromise(fetch(url, {
4416
+ headers: { Authorization: `Bearer ${opts.token}` }
4417
+ }).then(async (response) => {
4418
+ if (!response.ok) {
4419
+ const body = await response.text().catch(() => "");
4420
+ throw new Error(`HTTP ${response.status}: ${body}`);
4421
+ }
4422
+ return response.json();
4423
+ }), toCliError({ message: "Search request failed", code: "NETWORK_ERROR" }));
4424
+ }
4425
+ function fetchSessionCommits(opts) {
4426
+ const url = `${opts.workerUrl}/api/sessions/${opts.sessionId}/commits`;
4427
+ return ResultAsync.fromPromise(fetch(url, {
4428
+ headers: { Authorization: `Bearer ${opts.token}` }
4429
+ }).then(async (response) => {
4430
+ if (!response.ok)
4431
+ return [];
4432
+ const data = await response.json();
4433
+ return data.commits;
4434
+ }), toCliError({
4435
+ message: "Failed to fetch session commits",
4436
+ code: "NETWORK_ERROR"
4437
+ })).orElse(() => ok([]));
4438
+ }
4439
+ function extractSessionId(filename) {
4440
+ const match = filename.match(/(?:sessions|search)\/(.+?)\.(?:json|txt)$/);
4441
+ return match ? match[1] : filename;
4442
+ }
4443
+ function truncate(opts) {
4444
+ if (opts.text.length <= opts.maxLength)
4445
+ return opts.text;
4446
+ return opts.text.slice(0, opts.maxLength) + "...";
4447
+ }
4448
+ function formatSnippet(text) {
4449
+ const cleaned = text.replace(/\\n/g, " ").replace(/\\"/g, '"').replace(/\s+/g, " ").trim();
4450
+ return truncate({ text: cleaned, maxLength: 200 });
4451
+ }
4452
+ function buildCommitUrl(opts) {
4453
+ return `${opts.workerUrl}/app/${opts.org}/${opts.repo}/${opts.sha}`;
4454
+ }
4455
+ function renderSearchResults(opts) {
4456
+ if (opts.results.data.length === 0) {
4457
+ log7.info("No results found.");
4458
+ return;
4459
+ }
4460
+ log7.info(`${opts.results.data.length} result(s) for "${opts.results.search_query}"
4461
+ `);
4462
+ for (const item of opts.results.data) {
4463
+ const sessionId = extractSessionId(item.filename);
4464
+ const scorePercent = (item.score * 100).toFixed(1);
4465
+ log7.info(` ${sessionId} [${scorePercent}%]`);
4466
+ const snippet = item.content[0]?.text;
4467
+ if (snippet) {
4468
+ log7.info(` ${formatSnippet(snippet)}`);
4469
+ }
4470
+ const commits = opts.commitMap.get(sessionId) ?? [];
4471
+ if (commits.length > 0) {
4472
+ for (const commit of commits) {
4473
+ const url = buildCommitUrl({
4474
+ workerUrl: opts.workerUrl,
4475
+ org: commit.org,
4476
+ repo: commit.repo,
4477
+ sha: commit.commit_sha
4478
+ });
4479
+ log7.info(` -> ${url}`);
4480
+ }
4481
+ }
4482
+ log7.info("");
4483
+ }
4484
+ }
4485
+ function renderAiSearchResults(opts) {
4486
+ if (opts.results.response) {
4487
+ log7.info(opts.results.response);
4488
+ log7.info("");
4489
+ }
4490
+ if (opts.results.data.length > 0) {
4491
+ log7.info(`--- Sources (${opts.results.data.length}) ---
4492
+ `);
4493
+ for (const item of opts.results.data) {
4494
+ const sessionId = extractSessionId(item.filename);
4495
+ const scorePercent = (item.score * 100).toFixed(1);
4496
+ log7.info(` ${sessionId} [${scorePercent}%]`);
4497
+ const commits = opts.commitMap.get(sessionId) ?? [];
4498
+ if (commits.length > 0) {
4499
+ for (const commit of commits) {
4500
+ const url = buildCommitUrl({
4501
+ workerUrl: opts.workerUrl,
4502
+ org: commit.org,
4503
+ repo: commit.repo,
4504
+ sha: commit.commit_sha
4505
+ });
4506
+ log7.info(` -> ${url}`);
4507
+ }
4508
+ }
4509
+ }
4510
+ log7.info("");
4511
+ }
4512
+ }
4513
+ function isAiSearchResponse(response) {
4514
+ return "response" in response;
4515
+ }
4516
+ function search(opts) {
4517
+ return safeTry(async function* () {
4518
+ const config = yield* resolveConfig();
4519
+ if (!config) {
4520
+ return err(new CliError({
4521
+ message: "Not configured. Run 'residue login' first.",
4522
+ code: "CONFIG_MISSING"
4523
+ }));
4524
+ }
4525
+ const results = yield* fetchSearch({
4526
+ workerUrl: config.worker_url,
4527
+ token: config.token,
4528
+ query: opts.query,
4529
+ isAi: opts.isAi ?? false
4530
+ });
4531
+ const sessionIds = results.data.map((item) => extractSessionId(item.filename));
4532
+ const uniqueSessionIds = [...new Set(sessionIds)];
4533
+ const commitResults = yield* ResultAsync.combine(uniqueSessionIds.map((sessionId) => fetchSessionCommits({
4534
+ workerUrl: config.worker_url,
4535
+ token: config.token,
4536
+ sessionId
4537
+ }).map((commits) => ({ sessionId, commits }))));
4538
+ const commitMap = new Map;
4539
+ for (const entry of commitResults) {
4540
+ commitMap.set(entry.sessionId, entry.commits);
4541
+ }
4542
+ if (opts.isAi && isAiSearchResponse(results)) {
4543
+ renderAiSearchResults({
4544
+ results,
4545
+ commitMap,
4546
+ workerUrl: config.worker_url
4547
+ });
4548
+ } else {
4549
+ renderSearchResults({
4550
+ results,
4551
+ commitMap,
4552
+ workerUrl: config.worker_url
4553
+ });
4554
+ }
4555
+ return ok(undefined);
4556
+ });
4557
+ }
4558
+
4178
4559
  // src/commands/session-end.ts
4179
- var log6 = createLogger("session");
4560
+ var log8 = createLogger("session");
4180
4561
  function sessionEnd(opts) {
4181
4562
  return safeTry(async function* () {
4182
4563
  const projectRoot = yield* getProjectRoot();
@@ -4193,13 +4574,13 @@ function sessionEnd(opts) {
4193
4574
  id: opts.id,
4194
4575
  updates: { status: "ended" }
4195
4576
  });
4196
- log6.debug("session %s ended", opts.id);
4577
+ log8.debug("session %s ended", opts.id);
4197
4578
  return ok(undefined);
4198
4579
  });
4199
4580
  }
4200
4581
 
4201
4582
  // src/commands/session-start.ts
4202
- var log7 = createLogger("session");
4583
+ var log9 = createLogger("session");
4203
4584
  function sessionStart(opts) {
4204
4585
  const id = crypto.randomUUID();
4205
4586
  return getProjectRoot().andThen(getPendingPath).andThen((pendingPath) => addSession({
@@ -4214,7 +4595,7 @@ function sessionStart(opts) {
4214
4595
  }
4215
4596
  })).map(() => {
4216
4597
  process.stdout.write(id);
4217
- log7.debug("session started for %s", opts.agent);
4598
+ log9.debug("session started for %s", opts.agent);
4218
4599
  });
4219
4600
  }
4220
4601
 
@@ -4363,7 +4744,7 @@ export default function (pi: ExtensionAPI) {
4363
4744
  `;
4364
4745
 
4365
4746
  // src/commands/setup.ts
4366
- var log8 = createLogger("setup");
4747
+ var log10 = createLogger("setup");
4367
4748
  var CLAUDE_HOOK_COMMAND = "residue hook claude-code";
4368
4749
  function hasResidueHook(entries) {
4369
4750
  return entries.some((entry) => entry.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND));
@@ -4408,12 +4789,12 @@ function setupClaudeCode(projectRoot) {
4408
4789
  isChanged = true;
4409
4790
  }
4410
4791
  if (!isChanged) {
4411
- log8.info("residue hooks already configured in .claude/settings.json");
4792
+ log10.info("residue hooks already configured in .claude/settings.json");
4412
4793
  return;
4413
4794
  }
4414
4795
  await writeFile3(settingsPath, JSON.stringify(settings, null, 2) + `
4415
4796
  `);
4416
- log8.info("Configured Claude Code hooks in .claude/settings.json");
4797
+ log10.info("Configured Claude Code hooks in .claude/settings.json");
4417
4798
  })(), toCliError({ message: "Failed to setup Claude Code", code: "IO_ERROR" }));
4418
4799
  }
4419
4800
  function setupPi(projectRoot) {
@@ -4427,11 +4808,11 @@ function setupPi(projectRoot) {
4427
4808
  isExisting = true;
4428
4809
  } catch {}
4429
4810
  if (isExisting) {
4430
- log8.info("residue extension already exists at .pi/extensions/residue.ts");
4811
+ log10.info("residue extension already exists at .pi/extensions/residue.ts");
4431
4812
  return;
4432
4813
  }
4433
4814
  await writeFile3(targetPath, extension_ts_default);
4434
- log8.info("Installed pi extension at .pi/extensions/residue.ts");
4815
+ log10.info("Installed pi extension at .pi/extensions/residue.ts");
4435
4816
  })(), toCliError({ message: "Failed to setup pi", code: "IO_ERROR" }));
4436
4817
  }
4437
4818
  function setup(opts) {
@@ -4450,9 +4831,144 @@ function setup(opts) {
4450
4831
  });
4451
4832
  }
4452
4833
 
4834
+ // src/commands/status.ts
4835
+ import { readFile as readFile4, stat as stat5 } from "fs/promises";
4836
+ import { join as join6 } from "path";
4837
+ var log11 = createLogger("status");
4838
+ function checkFileExists(path) {
4839
+ return ResultAsync.fromPromise(stat5(path).then(() => true), toCliError({ message: "Failed to check file", code: "IO_ERROR" })).orElse(() => okAsync(false));
4840
+ }
4841
+ function checkHookInstalled(opts) {
4842
+ const hookPath = join6(opts.gitDir, "hooks", opts.hookName);
4843
+ return ResultAsync.fromPromise(readFile4(hookPath, "utf-8").then((content) => content.includes(opts.needle)), toCliError({ message: "Failed to read hook", code: "IO_ERROR" })).orElse(() => okAsync(false));
4844
+ }
4845
+ function getGitDir() {
4846
+ return ResultAsync.fromPromise((async () => {
4847
+ const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
4848
+ stdout: "pipe",
4849
+ stderr: "pipe"
4850
+ });
4851
+ await proc.exited;
4852
+ return (await new Response(proc.stdout).text()).trim();
4853
+ })(), toCliError({ message: "Failed to get git directory", code: "GIT_ERROR" }));
4854
+ }
4855
+ function status() {
4856
+ return safeTry(async function* () {
4857
+ const isRepo = yield* isGitRepo();
4858
+ if (!isRepo) {
4859
+ log11.info("Not a git repository.");
4860
+ return ok(undefined);
4861
+ }
4862
+ const projectRoot = yield* getProjectRoot();
4863
+ log11.info("Login");
4864
+ const globalConfig = yield* readConfig();
4865
+ if (globalConfig) {
4866
+ log11.info(` global: ${globalConfig.worker_url}`);
4867
+ } else {
4868
+ log11.info(" global: not configured");
4869
+ }
4870
+ const localConfig = yield* readLocalConfig(projectRoot);
4871
+ if (localConfig) {
4872
+ log11.info(` local: ${localConfig.worker_url}`);
4873
+ } else {
4874
+ log11.info(" local: not configured");
4875
+ }
4876
+ const isActiveConfig = localConfig ?? globalConfig;
4877
+ if (isActiveConfig) {
4878
+ log11.info(` active: ${isActiveConfig.worker_url}`);
4879
+ } else {
4880
+ log11.info(' active: none (run "residue login" to configure)');
4881
+ }
4882
+ log11.info("");
4883
+ log11.info("Hooks");
4884
+ const gitDir = yield* getGitDir();
4885
+ const isPostCommitInstalled = yield* checkHookInstalled({
4886
+ gitDir,
4887
+ hookName: "post-commit",
4888
+ needle: "residue capture"
4889
+ });
4890
+ log11.info(` post-commit: ${isPostCommitInstalled ? "installed" : "not installed"}`);
4891
+ const isPrePushInstalled = yield* checkHookInstalled({
4892
+ gitDir,
4893
+ hookName: "pre-push",
4894
+ needle: "residue sync"
4895
+ });
4896
+ log11.info(` pre-push: ${isPrePushInstalled ? "installed" : "not installed"}`);
4897
+ if (!isPostCommitInstalled || !isPrePushInstalled) {
4898
+ log11.info(' run "residue init" to install missing hooks');
4899
+ }
4900
+ log11.info("");
4901
+ log11.info("Adapters");
4902
+ const isClaudeSetup = yield* checkFileExists(join6(projectRoot, ".claude", "settings.json"));
4903
+ let isClaudeHookConfigured = false;
4904
+ if (isClaudeSetup) {
4905
+ isClaudeHookConfigured = yield* ResultAsync.fromPromise(readFile4(join6(projectRoot, ".claude", "settings.json"), "utf-8").then((content) => content.includes("residue hook claude-code")), toCliError({
4906
+ message: "Failed to read claude settings",
4907
+ code: "IO_ERROR"
4908
+ })).orElse(() => okAsync(false));
4909
+ }
4910
+ log11.info(` claude-code: ${isClaudeHookConfigured ? "configured" : "not configured"}`);
4911
+ const isPiSetup = yield* checkFileExists(join6(projectRoot, ".pi", "extensions", "residue.ts"));
4912
+ log11.info(` pi: ${isPiSetup ? "configured" : "not configured"}`);
4913
+ log11.info("");
4914
+ log11.info("Sessions");
4915
+ const pendingPath = yield* getPendingPath(projectRoot);
4916
+ const sessions = yield* readPending(pendingPath);
4917
+ if (sessions.length === 0) {
4918
+ log11.info(" no pending sessions");
4919
+ } else {
4920
+ const openSessions = sessions.filter((s) => s.status === "open");
4921
+ const endedSessions = sessions.filter((s) => s.status === "ended");
4922
+ const totalCommits = sessions.reduce((sum, s) => sum + s.commits.length, 0);
4923
+ const sessionsWithCommits = sessions.filter((s) => s.commits.length > 0);
4924
+ log11.info(` total: ${sessions.length}`);
4925
+ log11.info(` open: ${openSessions.length}`);
4926
+ log11.info(` ended: ${endedSessions.length}`);
4927
+ log11.info(` commits: ${totalCommits} across ${sessionsWithCommits.length} session(s)`);
4928
+ const isReadyToSync = sessionsWithCommits.length > 0;
4929
+ if (isReadyToSync) {
4930
+ log11.info(` ${sessionsWithCommits.length} session(s) ready to sync on next push`);
4931
+ } else {
4932
+ log11.info(" no sessions ready to sync (no commits captured yet)");
4933
+ }
4934
+ }
4935
+ return ok(undefined);
4936
+ });
4937
+ }
4938
+ // package.json
4939
+ var package_default = {
4940
+ name: "@residue/cli",
4941
+ version: "0.0.4",
4942
+ repository: {
4943
+ type: "git",
4944
+ url: "https://github.com/butttons/residue",
4945
+ directory: "packages/cli"
4946
+ },
4947
+ type: "module",
4948
+ bin: {
4949
+ residue: "./dist/index.js"
4950
+ },
4951
+ scripts: {
4952
+ dev: "bun run src/index.ts",
4953
+ build: "bun build src/index.ts --outfile dist/index.js --target bun",
4954
+ test: "bun test",
4955
+ typecheck: "tsc --noEmit"
4956
+ },
4957
+ devDependencies: {
4958
+ "@types/bun": "latest",
4959
+ "@types/debug": "^4.1.12",
4960
+ typescript: "^5.7.0"
4961
+ },
4962
+ dependencies: {
4963
+ commander: "^14.0.3",
4964
+ debug: "^4.4.3",
4965
+ neverthrow: "^8.2.0"
4966
+ }
4967
+ };
4968
+
4453
4969
  // src/index.ts
4454
4970
  var program2 = new Command;
4455
- program2.name("residue").description("Capture AI agent conversations linked to git commits").version("0.0.1");
4971
+ program2.name("residue").description("Capture AI agent conversations linked to git commits").version(package_default.version);
4456
4972
  program2.command("login").description("Save worker URL and auth token").requiredOption("--url <worker_url>", "Worker URL").requiredOption("--token <auth_token>", "Auth token").option("--local", "Save config to this project instead of globally").action(wrapCommand((opts) => login({ url: opts.url, token: opts.token, isLocal: opts.local })));
4457
4973
  program2.command("init").description("Install git hooks in current repo").action(wrapCommand(() => init()));
4458
4974
  program2.command("setup").description("Configure an agent adapter for this project").argument("<agent>", "Agent to set up (claude-code, pi)").action(wrapCommand((agent) => setup({ agent })));
@@ -4468,4 +4984,7 @@ session.command("end").description("Mark an agent session as ended").requiredOpt
4468
4984
  program2.command("capture").description("Tag pending sessions with current commit SHA (called by post-commit hook)").action(wrapHookCommand(() => capture()));
4469
4985
  program2.command("sync").description("Upload pending sessions to worker (called by pre-push hook)").option("--remote-url <url>", "Remote URL (passed by pre-push hook)").action(wrapHookCommand((opts) => sync({ remoteUrl: opts.remoteUrl })));
4470
4986
  program2.command("push").description("Upload pending sessions to worker (manual trigger)").action(wrapCommand(() => push()));
4987
+ program2.command("clear").description("Remove pending sessions from the local queue").option("--id <session-id>", "Clear a specific session by ID").action(wrapCommand((opts) => clear({ id: opts.id })));
4988
+ program2.command("status").description("Show current residue state for this project").action(wrapCommand(() => status()));
4989
+ program2.command("search").description("Search session history").argument("<query>", "Search query").option("--ai", "Use AI-powered search (generates an answer with citations)").action(wrapCommand((query, opts) => search({ query, isAi: opts.ai })));
4471
4990
  program2.parse();