@residue/cli 0.0.3 → 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) {
@@ -4453,7 +4834,7 @@ function setup(opts) {
4453
4834
  // src/commands/status.ts
4454
4835
  import { readFile as readFile4, stat as stat5 } from "fs/promises";
4455
4836
  import { join as join6 } from "path";
4456
- var log9 = createLogger("status");
4837
+ var log11 = createLogger("status");
4457
4838
  function checkFileExists(path) {
4458
4839
  return ResultAsync.fromPromise(stat5(path).then(() => true), toCliError({ message: "Failed to check file", code: "IO_ERROR" })).orElse(() => okAsync(false));
4459
4840
  }
@@ -4475,49 +4856,49 @@ function status() {
4475
4856
  return safeTry(async function* () {
4476
4857
  const isRepo = yield* isGitRepo();
4477
4858
  if (!isRepo) {
4478
- log9.info("Not a git repository.");
4859
+ log11.info("Not a git repository.");
4479
4860
  return ok(undefined);
4480
4861
  }
4481
4862
  const projectRoot = yield* getProjectRoot();
4482
- log9.info("Login");
4863
+ log11.info("Login");
4483
4864
  const globalConfig = yield* readConfig();
4484
4865
  if (globalConfig) {
4485
- log9.info(` global: ${globalConfig.worker_url}`);
4866
+ log11.info(` global: ${globalConfig.worker_url}`);
4486
4867
  } else {
4487
- log9.info(" global: not configured");
4868
+ log11.info(" global: not configured");
4488
4869
  }
4489
4870
  const localConfig = yield* readLocalConfig(projectRoot);
4490
4871
  if (localConfig) {
4491
- log9.info(` local: ${localConfig.worker_url}`);
4872
+ log11.info(` local: ${localConfig.worker_url}`);
4492
4873
  } else {
4493
- log9.info(" local: not configured");
4874
+ log11.info(" local: not configured");
4494
4875
  }
4495
4876
  const isActiveConfig = localConfig ?? globalConfig;
4496
4877
  if (isActiveConfig) {
4497
- log9.info(` active: ${isActiveConfig.worker_url}`);
4878
+ log11.info(` active: ${isActiveConfig.worker_url}`);
4498
4879
  } else {
4499
- log9.info(' active: none (run "residue login" to configure)');
4880
+ log11.info(' active: none (run "residue login" to configure)');
4500
4881
  }
4501
- log9.info("");
4502
- log9.info("Hooks");
4882
+ log11.info("");
4883
+ log11.info("Hooks");
4503
4884
  const gitDir = yield* getGitDir();
4504
4885
  const isPostCommitInstalled = yield* checkHookInstalled({
4505
4886
  gitDir,
4506
4887
  hookName: "post-commit",
4507
4888
  needle: "residue capture"
4508
4889
  });
4509
- log9.info(` post-commit: ${isPostCommitInstalled ? "installed" : "not installed"}`);
4890
+ log11.info(` post-commit: ${isPostCommitInstalled ? "installed" : "not installed"}`);
4510
4891
  const isPrePushInstalled = yield* checkHookInstalled({
4511
4892
  gitDir,
4512
4893
  hookName: "pre-push",
4513
4894
  needle: "residue sync"
4514
4895
  });
4515
- log9.info(` pre-push: ${isPrePushInstalled ? "installed" : "not installed"}`);
4896
+ log11.info(` pre-push: ${isPrePushInstalled ? "installed" : "not installed"}`);
4516
4897
  if (!isPostCommitInstalled || !isPrePushInstalled) {
4517
- log9.info(' run "residue init" to install missing hooks');
4898
+ log11.info(' run "residue init" to install missing hooks');
4518
4899
  }
4519
- log9.info("");
4520
- log9.info("Adapters");
4900
+ log11.info("");
4901
+ log11.info("Adapters");
4521
4902
  const isClaudeSetup = yield* checkFileExists(join6(projectRoot, ".claude", "settings.json"));
4522
4903
  let isClaudeHookConfigured = false;
4523
4904
  if (isClaudeSetup) {
@@ -4526,29 +4907,29 @@ function status() {
4526
4907
  code: "IO_ERROR"
4527
4908
  })).orElse(() => okAsync(false));
4528
4909
  }
4529
- log9.info(` claude-code: ${isClaudeHookConfigured ? "configured" : "not configured"}`);
4910
+ log11.info(` claude-code: ${isClaudeHookConfigured ? "configured" : "not configured"}`);
4530
4911
  const isPiSetup = yield* checkFileExists(join6(projectRoot, ".pi", "extensions", "residue.ts"));
4531
- log9.info(` pi: ${isPiSetup ? "configured" : "not configured"}`);
4532
- log9.info("");
4533
- log9.info("Sessions");
4912
+ log11.info(` pi: ${isPiSetup ? "configured" : "not configured"}`);
4913
+ log11.info("");
4914
+ log11.info("Sessions");
4534
4915
  const pendingPath = yield* getPendingPath(projectRoot);
4535
4916
  const sessions = yield* readPending(pendingPath);
4536
4917
  if (sessions.length === 0) {
4537
- log9.info(" no pending sessions");
4918
+ log11.info(" no pending sessions");
4538
4919
  } else {
4539
4920
  const openSessions = sessions.filter((s) => s.status === "open");
4540
4921
  const endedSessions = sessions.filter((s) => s.status === "ended");
4541
4922
  const totalCommits = sessions.reduce((sum, s) => sum + s.commits.length, 0);
4542
4923
  const sessionsWithCommits = sessions.filter((s) => s.commits.length > 0);
4543
- log9.info(` total: ${sessions.length}`);
4544
- log9.info(` open: ${openSessions.length}`);
4545
- log9.info(` ended: ${endedSessions.length}`);
4546
- log9.info(` commits: ${totalCommits} across ${sessionsWithCommits.length} session(s)`);
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)`);
4547
4928
  const isReadyToSync = sessionsWithCommits.length > 0;
4548
4929
  if (isReadyToSync) {
4549
- log9.info(` ${sessionsWithCommits.length} session(s) ready to sync on next push`);
4930
+ log11.info(` ${sessionsWithCommits.length} session(s) ready to sync on next push`);
4550
4931
  } else {
4551
- log9.info(" no sessions ready to sync (no commits captured yet)");
4932
+ log11.info(" no sessions ready to sync (no commits captured yet)");
4552
4933
  }
4553
4934
  }
4554
4935
  return ok(undefined);
@@ -4557,7 +4938,7 @@ function status() {
4557
4938
  // package.json
4558
4939
  var package_default = {
4559
4940
  name: "@residue/cli",
4560
- version: "0.0.3",
4941
+ version: "0.0.4",
4561
4942
  repository: {
4562
4943
  type: "git",
4563
4944
  url: "https://github.com/butttons/residue",
@@ -4603,5 +4984,7 @@ session.command("end").description("Mark an agent session as ended").requiredOpt
4603
4984
  program2.command("capture").description("Tag pending sessions with current commit SHA (called by post-commit hook)").action(wrapHookCommand(() => capture()));
4604
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 })));
4605
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 })));
4606
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 })));
4607
4990
  program2.parse();