sisyphi 1.2.16 → 1.2.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.
package/README.md CHANGED
@@ -330,13 +330,13 @@ Run `configure-upload` with that URL to write credentials to `~/.sisyphus/config
330
330
 
331
331
  ```bash
332
332
  # Safest — no argv leak:
333
- pbpaste | sis admin configure-upload --stdin
333
+ pbpaste | sis admin report configure-upload --stdin
334
334
 
335
335
  # Interactive prompt:
336
- sis admin configure-upload
336
+ sis admin report configure-upload
337
337
 
338
338
  # Direct argv (triggers a leak warning — token visible via `ps` and shell history):
339
- sis admin configure-upload "https://<worker-host>/upload?token=sisyphus_pat_..."
339
+ sis admin report configure-upload "https://<worker-host>/upload?token=sisyphus_pat_..."
340
340
  ```
341
341
 
342
342
  **Config** — `configure-upload` always writes to `~/.sisyphus/config.json`. The `upload` block is only honored from the global config; a project-local `.sisyphus/config.json` with an `upload` block is ignored with a warning (security hardening — prevents project files from redirecting your uploads).
package/dist/cli.js CHANGED
@@ -1038,6 +1038,51 @@ var init_version = __esm({
1038
1038
  }
1039
1039
  });
1040
1040
 
1041
+ // src/shared/upload.ts
1042
+ import { readFile } from "fs/promises";
1043
+ function parseWorkerError(body) {
1044
+ try {
1045
+ const parsed = JSON.parse(body);
1046
+ if (parsed && typeof parsed.error === "string") return parsed.error;
1047
+ } catch {
1048
+ }
1049
+ return body;
1050
+ }
1051
+ function isUploadConfigured(upload) {
1052
+ return !!upload && upload.url.length > 0 && upload.token.length > 0;
1053
+ }
1054
+ async function uploadSession(args2) {
1055
+ const { config, zipPath, manifest } = args2;
1056
+ const formData = new FormData();
1057
+ formData.append("manifest", new Blob([JSON.stringify(manifest)], { type: "application/json" }));
1058
+ formData.append("bundle", new Blob([await readFile(zipPath)], { type: "application/zip" }), `${manifest.sessionId}.zip`);
1059
+ const res = await fetch(`${config.url}/upload`, {
1060
+ method: "POST",
1061
+ headers: { Authorization: `Bearer ${config.token}` },
1062
+ body: formData
1063
+ });
1064
+ if (!res.ok) {
1065
+ const rawBody = await res.text();
1066
+ const body = rawBody.length > 4096 ? rawBody.slice(0, 4096) + "\u2026 [truncated]" : rawBody;
1067
+ throw new UploadError(res.status, body);
1068
+ }
1069
+ return res.json();
1070
+ }
1071
+ var UploadError;
1072
+ var init_upload = __esm({
1073
+ "src/shared/upload.ts"() {
1074
+ "use strict";
1075
+ UploadError = class extends Error {
1076
+ constructor(status, rawBody) {
1077
+ const parsed = parseWorkerError(rawBody);
1078
+ super(`HTTP ${status}: ${parsed}`);
1079
+ this.status = status;
1080
+ }
1081
+ status;
1082
+ };
1083
+ }
1084
+ });
1085
+
1041
1086
  // src/shared/session-export.ts
1042
1087
  import { execFile as execFile2 } from "child_process";
1043
1088
  import { promisify } from "util";
@@ -1166,51 +1211,6 @@ var init_session_export = __esm({
1166
1211
  }
1167
1212
  });
1168
1213
 
1169
- // src/shared/upload.ts
1170
- import { readFile } from "fs/promises";
1171
- function parseWorkerError(body) {
1172
- try {
1173
- const parsed = JSON.parse(body);
1174
- if (parsed && typeof parsed.error === "string") return parsed.error;
1175
- } catch {
1176
- }
1177
- return body;
1178
- }
1179
- function isUploadConfigured(upload) {
1180
- return !!upload && upload.url.length > 0 && upload.token.length > 0;
1181
- }
1182
- async function uploadSession(args2) {
1183
- const { config, zipPath, manifest } = args2;
1184
- const formData = new FormData();
1185
- formData.append("manifest", new Blob([JSON.stringify(manifest)], { type: "application/json" }));
1186
- formData.append("bundle", new Blob([await readFile(zipPath)], { type: "application/zip" }), `${manifest.sessionId}.zip`);
1187
- const res = await fetch(`${config.url}/upload`, {
1188
- method: "POST",
1189
- headers: { Authorization: `Bearer ${config.token}` },
1190
- body: formData
1191
- });
1192
- if (!res.ok) {
1193
- const rawBody = await res.text();
1194
- const body = rawBody.length > 4096 ? rawBody.slice(0, 4096) + "\u2026 [truncated]" : rawBody;
1195
- throw new UploadError(res.status, body);
1196
- }
1197
- return res.json();
1198
- }
1199
- var UploadError;
1200
- var init_upload = __esm({
1201
- "src/shared/upload.ts"() {
1202
- "use strict";
1203
- UploadError = class extends Error {
1204
- constructor(status, rawBody) {
1205
- const parsed = parseWorkerError(rawBody);
1206
- super(`HTTP ${status}: ${parsed}`);
1207
- this.status = status;
1208
- }
1209
- status;
1210
- };
1211
- }
1212
- });
1213
-
1214
1214
  // src/shared/manifest.ts
1215
1215
  import os2 from "os";
1216
1216
  function mapEffortFallback(level) {
@@ -2446,12 +2446,12 @@ var init_tailscale = __esm({
2446
2446
  });
2447
2447
 
2448
2448
  // src/cli/deploy/runner.ts
2449
- import { spawn as spawn2, spawnSync as spawnSync3 } from "child_process";
2449
+ import { spawn as spawn2, spawnSync as spawnSync5 } from "child_process";
2450
2450
  import { copyFileSync as copyFileSync3, existsSync as existsSync34, mkdirSync as mkdirSync17, readFileSync as readFileSync34 } from "fs";
2451
2451
  function runTerraform(provider, args2, extraEnv) {
2452
2452
  ensureProviderStateDir(provider);
2453
2453
  ensureTerraformInstalled();
2454
- const result = spawnSync3("terraform", args2, {
2454
+ const result = spawnSync5("terraform", args2, {
2455
2455
  cwd: providerModuleDir(provider),
2456
2456
  stdio: "inherit",
2457
2457
  env: { ...EXEC_ENV, ...extraEnv }
@@ -2460,7 +2460,7 @@ function runTerraform(provider, args2, extraEnv) {
2460
2460
  return result.status === null ? 1 : result.status;
2461
2461
  }
2462
2462
  function ensureTerraformInstalled() {
2463
- const result = spawnSync3("terraform", ["version"], { stdio: "pipe", env: EXEC_ENV });
2463
+ const result = spawnSync5("terraform", ["version"], { stdio: "pipe", env: EXEC_ENV });
2464
2464
  if (result.error || result.status !== 0) {
2465
2465
  const platform = process.platform;
2466
2466
  const hint = platform === "darwin" ? "brew install terraform" : "See https://developer.hashicorp.com/terraform/install";
@@ -2488,7 +2488,7 @@ or pass --ssh-key <path>.`
2488
2488
  return readFileSync34(path, "utf-8").trim();
2489
2489
  }
2490
2490
  function readOutputs(provider) {
2491
- const result = spawnSync3("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
2491
+ const result = spawnSync5("terraform", ["output", "-json", `-state=${deployStatePath(provider)}`], {
2492
2492
  cwd: providerModuleDir(provider),
2493
2493
  encoding: "utf-8",
2494
2494
  env: EXEC_ENV
@@ -2672,7 +2672,7 @@ function effectiveSshTarget(provider) {
2672
2672
  }
2673
2673
  function deploySsh(provider, remoteCmd) {
2674
2674
  const target = effectiveSshTarget(provider);
2675
- const moshAvailable = spawnSync3("mosh", ["--version"], { stdio: "pipe", env: EXEC_ENV }).status === 0;
2675
+ const moshAvailable = spawnSync5("mosh", ["--version"], { stdio: "pipe", env: EXEC_ENV }).status === 0;
2676
2676
  const bin = moshAvailable && remoteCmd.length === 0 ? "mosh" : "ssh";
2677
2677
  const args2 = remoteCmd.length > 0 ? [target, ...remoteCmd] : [target];
2678
2678
  const child = spawn2(bin, args2, { stdio: "inherit", env: EXEC_ENV });
@@ -2734,10 +2734,10 @@ var init_runner = __esm({
2734
2734
  });
2735
2735
 
2736
2736
  // src/cli/deploy/ssh-exec.ts
2737
- import { spawn as spawn3, spawnSync as spawnSync4 } from "child_process";
2737
+ import { spawn as spawn3, spawnSync as spawnSync6 } from "child_process";
2738
2738
  function runOnBox(provider, cmd) {
2739
2739
  const target = effectiveSshTarget(provider);
2740
- const result = spawnSync4("ssh", [target, cmd], {
2740
+ const result = spawnSync6("ssh", [target, cmd], {
2741
2741
  encoding: "utf-8",
2742
2742
  env: EXEC_ENV
2743
2743
  });
@@ -2798,11 +2798,11 @@ var init_grove = __esm({
2798
2798
  });
2799
2799
 
2800
2800
  // src/cli/cloud/repo.ts
2801
- import { spawnSync as spawnSync5 } from "child_process";
2801
+ import { spawnSync as spawnSync7 } from "child_process";
2802
2802
  import { existsSync as existsSync35 } from "fs";
2803
2803
  import { basename as basename8, join as join30 } from "path";
2804
2804
  function captureGit(args2, cwd) {
2805
- const result = spawnSync5("git", args2, {
2805
+ const result = spawnSync7("git", args2, {
2806
2806
  encoding: "utf-8",
2807
2807
  env: EXEC_ENV,
2808
2808
  cwd: cwd ?? process.cwd()
@@ -9902,13 +9902,16 @@ Exit codes: 0 ok`
9902
9902
  }
9903
9903
 
9904
9904
  // src/cli/commands/bug.ts
9905
- import { execFileSync as execFileSync3, spawnSync as spawnSync2 } from "child_process";
9906
- import { existsSync as existsSync20, readFileSync as readFileSync22 } from "fs";
9907
- import os from "os";
9905
+ import { spawnSync as spawnSync3 } from "child_process";
9906
+
9907
+ // src/cli/commands/report-shared.ts
9908
9908
  init_version();
9909
9909
  init_platform();
9910
9910
  init_paths();
9911
9911
  init_state();
9912
+ import { execFileSync as execFileSync3, spawnSync as spawnSync2 } from "child_process";
9913
+ import { existsSync as existsSync20, readFileSync as readFileSync22 } from "fs";
9914
+ import os from "os";
9912
9915
  var REPO = "crouton-labs/sisyphus";
9913
9916
  function tryCmd(bin, args2) {
9914
9917
  try {
@@ -9974,8 +9977,8 @@ function tailLog(lines) {
9974
9977
  return null;
9975
9978
  }
9976
9979
  }
9977
- function deriveTitle(description) {
9978
- const firstLine = description.split("\n").map((l) => l.trim()).find(Boolean) ?? "Bug report";
9980
+ function deriveTitle(description, fallback = "Bug report") {
9981
+ const firstLine = description.split("\n").map((l) => l.trim()).find(Boolean) ?? fallback;
9979
9982
  return firstLine.length > 80 ? firstLine.slice(0, 77) + "..." : firstLine;
9980
9983
  }
9981
9984
  function buildBody(args2) {
@@ -9986,7 +9989,7 @@ function buildBody(args2) {
9986
9989
  ---
9987
9990
 
9988
9991
  <details>
9989
- <summary>Environment (auto-collected by <code>sis admin report bug</code>)</summary>
9992
+ <summary>Environment (auto-collected)</summary>
9990
9993
 
9991
9994
  | field | value |
9992
9995
  |---|---|
@@ -10028,7 +10031,7 @@ function fallbackUrl(title, body) {
10028
10031
  let b = encodeURIComponent(body);
10029
10032
  if (base.length + t.length + b.length > 7500) {
10030
10033
  b = encodeURIComponent(
10031
- body.split("\n\n---\n\n")[0] + "\n\n---\n\n_(diagnostics omitted \u2014 URL too long. Authenticate `gh` and re-run `sis admin report bug` to attach full telemetry.)_"
10034
+ body.split("\n\n---\n\n")[0] + "\n\n---\n\n_(diagnostics omitted \u2014 URL too long. Authenticate `gh` and re-run to attach full telemetry.)_"
10032
10035
  );
10033
10036
  }
10034
10037
  return `${base}?title=${t}&body=${b}`;
@@ -10038,6 +10041,8 @@ function ghReady() {
10038
10041
  const auth = spawnSync2("gh", ["auth", "status"], { stdio: "ignore", timeout: 5e3 });
10039
10042
  return auth.status === 0;
10040
10043
  }
10044
+
10045
+ // src/cli/commands/bug.ts
10041
10046
  function registerBug(program2) {
10042
10047
  program2.command("bug").description("Report a sisyphus bug \u2014 files a GitHub issue with feedback + diagnostics").argument("[description]", "What went wrong (omit to read from stdin)").option("--message <message>", "Bug description (alternative to the positional argument)").option("--stdin", "Read the description from stdin (avoids shell escaping for long reports)").option("--title <title>", "Issue title (default: first line of the description)").option("--session <id>", "Attach stats for a specific session (default: active session for cwd)").option("--no-session", "Do not attach any session stats").option("--logs [n]", "Attach the last N lines of daemon.log (default 50)").option("--cwd <path>", "Project directory used to find the active session", process.cwd()).addHelpText(
10043
10048
  "after",
@@ -10105,7 +10110,7 @@ Exit codes: 0 ok | 1 filing error | 2 usage`
10105
10110
  emitJsonOk({ url, filed: false });
10106
10111
  return;
10107
10112
  }
10108
- const result = spawnSync2(
10113
+ const result = spawnSync3(
10109
10114
  "gh",
10110
10115
  ["issue", "create", "--repo", REPO, "--title", title, "--body-file", "-"],
10111
10116
  { input: body, encoding: "utf-8", timeout: 3e4 }
@@ -10122,6 +10127,138 @@ Exit codes: 0 ok | 1 filing error | 2 usage`
10122
10127
  );
10123
10128
  }
10124
10129
 
10130
+ // src/cli/commands/feedback.ts
10131
+ import { spawnSync as spawnSync4 } from "child_process";
10132
+ init_config();
10133
+ init_upload();
10134
+ init_version();
10135
+ init_platform();
10136
+ async function sendToCloud(config, payload) {
10137
+ const res = await fetch(`${config.url}/feedback`, {
10138
+ method: "POST",
10139
+ headers: {
10140
+ Authorization: `Bearer ${config.token}`,
10141
+ "Content-Type": "application/json"
10142
+ },
10143
+ body: JSON.stringify(payload)
10144
+ });
10145
+ if (!res.ok) {
10146
+ const raw = await res.text();
10147
+ const body = raw.length > 2048 ? raw.slice(0, 2048) + "\u2026 [truncated]" : raw;
10148
+ let detail = body;
10149
+ try {
10150
+ const parsed = JSON.parse(body);
10151
+ if (typeof parsed.error === "string") detail = parsed.error;
10152
+ } catch {
10153
+ }
10154
+ throw new Error(`HTTP ${res.status}: ${detail}`);
10155
+ }
10156
+ return await res.json();
10157
+ }
10158
+ function registerFeedback(program2) {
10159
+ program2.command("feedback").description("Send feedback about sisyphus itself \u2014 private cloud if registered, else a GitHub issue").argument("[message]", "The feedback (omit to read from stdin)").option("--message <message>", "Feedback text (alternative to the positional argument)").option("--stdin", "Read the feedback from stdin (avoids shell escaping for long notes)").option("--session <id>", "Attach stats for a specific session (default: active session for cwd)").option("--no-session", "Do not attach any session stats").option("--cwd <path>", "Project directory used to find the active session", process.cwd()).addHelpText(
10160
+ "after",
10161
+ `
10162
+ feedback: report a problem or suggestion about SISYPHUS ITSELF \u2014 the tool, the
10163
+ CLI, the orchestration workflow \u2014 not about the user's own task or codebase.
10164
+
10165
+ When to use (agent guidance)
10166
+ Run this whenever the user gripes about sisyphus: a daemon crash, a CLI that
10167
+ misbehaved, a confusing or broken workflow, a rough edge, a feature wish.
10168
+ Filing it is a low-cost, helpful side-action \u2014 do it WITHOUT asking the user
10169
+ first. Summarize their complaint in your own words as the message.
10170
+ NOT for the user's project work, code bugs, or task questions.
10171
+
10172
+ Input
10173
+ [message] optional positional \u2014 the feedback (omit to read from stdin or --message).
10174
+ --message <msg> feedback text (alternative to the positional argument).
10175
+ --stdin read the feedback from stdin (avoids shell escaping for long notes).
10176
+ --session <id> attach stats for a specific session (default: active session for cwd).
10177
+ --no-session do not attach any session stats.
10178
+ --cwd <path> project directory used to find the active session (default: cwd).
10179
+
10180
+ Where it goes
10181
+ - Cloud registered (sis admin report configure-upload set up):
10182
+ POSTs privately to the upload worker's /feedback endpoint. Stays private.
10183
+ - Not registered: falls back to a GitHub issue against ${REPO}
10184
+ (via \`gh\` if authenticated, else a prefilled "new issue" URL). PUBLIC.
10185
+
10186
+ Telemetry attached (non-sensitive): sisyphus version, platform, and session
10187
+ STATS only (counts/status) \u2014 never task/goal/context text.
10188
+
10189
+ Output (stdout, JSON envelope)
10190
+ cloud: { ok, data: { sent: true, channel: "cloud", feedbackId, receivedAt } }
10191
+ github: { ok, data: { sent, channel: "github", issueUrl | url } }
10192
+
10193
+ Exit codes: 0 ok | 1 filing error | 2 usage | 60 cloud unreachable (retry-safe)`
10194
+ ).action(
10195
+ async (messageArg, opts) => {
10196
+ let message;
10197
+ if (opts.stdin) {
10198
+ message = await readStdin({ force: true });
10199
+ if (opts.message || messageArg) {
10200
+ exitUsage("stdin_conflict", "--stdin conflicts with --message / positional message; pass one source", {
10201
+ received: { stdin: true, message: opts.message ?? messageArg }
10202
+ });
10203
+ }
10204
+ } else {
10205
+ message = messageArg ?? opts.message ?? await readStdin();
10206
+ }
10207
+ if (!message || !message.trim()) {
10208
+ exitUsage("missing_message", "provide feedback (argument, --message, or piped stdin)", {
10209
+ next: 'sis feedback "what is wrong or could be better" \u2014 or: sis feedback --stdin < notes.md'
10210
+ });
10211
+ }
10212
+ message = message.trim();
10213
+ const sessionDisabled = opts.session === false;
10214
+ const session2 = sessionDisabled ? null : await resolveSessionStats(
10215
+ typeof opts.session === "string" ? opts.session : void 0,
10216
+ opts.cwd
10217
+ );
10218
+ const config = loadConfig(opts.cwd);
10219
+ if (isUploadConfigured(config.upload)) {
10220
+ try {
10221
+ const result2 = await sendToCloud(config.upload, {
10222
+ message,
10223
+ sisyphusVersion: getSisyphusVersion(),
10224
+ platform: platformLabel(),
10225
+ session: session2
10226
+ });
10227
+ emitJsonOk({ sent: true, channel: "cloud", ...result2 });
10228
+ return;
10229
+ } catch (err) {
10230
+ exitError({
10231
+ kind: "transient",
10232
+ code: "cloud_unreachable",
10233
+ message: `feedback upload failed: ${err.message}`,
10234
+ next: "retry shortly, or file publicly with `sis admin report bug`"
10235
+ });
10236
+ }
10237
+ }
10238
+ const env = collectEnv();
10239
+ const title = `Feedback: ${deriveTitle(message, "Feedback")}`;
10240
+ const body = buildBody({ description: message, env, session: session2, logTail: null });
10241
+ if (!ghReady()) {
10242
+ emitJsonOk({ sent: false, channel: "github", url: fallbackUrl(title, body) });
10243
+ return;
10244
+ }
10245
+ const result = spawnSync4(
10246
+ "gh",
10247
+ ["issue", "create", "--repo", REPO, "--title", title, "--body-file", "-"],
10248
+ { input: body, encoding: "utf-8", timeout: 3e4 }
10249
+ );
10250
+ if (result.status !== 0) {
10251
+ const url = fallbackUrl(title, body);
10252
+ const stderr = (result.stderr ?? "").trim();
10253
+ emitJsonOk({ sent: false, channel: "github", url, error: stderr });
10254
+ process.exit(1);
10255
+ }
10256
+ const issueUrl = (result.stdout ?? "").trim().split("\n").filter(Boolean).pop() ?? "";
10257
+ emitJsonOk({ sent: true, channel: "github", issueUrl });
10258
+ }
10259
+ );
10260
+ }
10261
+
10125
10262
  // src/cli/commands/init.ts
10126
10263
  import { existsSync as existsSync21, mkdirSync as mkdirSync9, writeFileSync as writeFileSync10 } from "fs";
10127
10264
  import { join as join20 } from "path";
@@ -13525,6 +13662,7 @@ var RUBRICS = {
13525
13662
  "companion": { short: "Companion-pane helper", useWhen: "driving the companion Claude pane" },
13526
13663
  "deploy": { short: "Provision cloud boxes (Terraform)", useWhen: "standing up or tearing down infra" },
13527
13664
  "cloud": { short: "Per-repo workflow on a deployed box", useWhen: "syncing work to/from the box" },
13665
+ "feedback": { short: "Report a problem with sisyphus itself", useWhen: "the user complains about the tool or workflow" },
13528
13666
  "session lifecycle": { short: "Start/stop/advance a session", useWhen: "changing whether a session runs" },
13529
13667
  "session inspect": { short: "Read session state", useWhen: "you need status/history/context without mutating" },
13530
13668
  "session config": { short: "Change a session's settings", useWhen: "adjusting task, effort, or dangerous mode" },
@@ -13783,13 +13921,14 @@ registerCompanionProfile(program.commands.find((c) => c.name() === "companion"))
13783
13921
  registerDeploy(program);
13784
13922
  registerDeployList(program.commands.find((c) => c.name() === "deploy"));
13785
13923
  registerCloud(program);
13924
+ registerFeedback(program);
13786
13925
  var diagnostic = program.command("diagnostic", { hidden: true });
13787
13926
  attachNotify(diagnostic);
13788
13927
  attachTmuxSessions(diagnostic);
13789
13928
  registerHomeInit(diagnostic);
13790
13929
  var args = process.argv.slice(2);
13791
13930
  var firstArg = args[0];
13792
- var skipWelcome = ["session", "start", "dashboard", "agent", "orch", "ask", "ui", "segment", "admin", "help", "--help", "--version"];
13931
+ var skipWelcome = ["session", "start", "dashboard", "agent", "orch", "ask", "ui", "segment", "admin", "feedback", "help", "--help", "--version"];
13793
13932
  if (!existsSync50(globalDir()) && firstArg && !skipWelcome.includes(firstArg)) {
13794
13933
  mkdirSync21(globalDir(), { recursive: true });
13795
13934
  console.log("");