@standardagents/code 0.9.6 → 0.9.8

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
@@ -104,6 +104,20 @@ standardcode -e -- <dir> # prompt for the endpoint for this run
104
104
  On first run it connects to the hosted Standard Code instance
105
105
  (`https://api.standardcode.ai`) and walks you through a one-time browser sign-in — press
106
106
  Enter, approve in the browser, and your key is stored in `~/.standardagents/credentials`.
107
+ ### Choosing an agent
108
+
109
+ Session start opens with an agent picker: **Frontier One** (coming soon),
110
+ **Unlimited One** (the default — adaptive high/low routing), **Sama One**
111
+ (OpenAI-only, powered by your own ChatGPT authorization), and **Dario**
112
+ (unavailable). Skip the menu with `--agent unlimited` or `--agent sama`.
113
+
114
+ Sama One is BYOK: the first time you pick it, the CLI opens
115
+ `https://standardcode.ai/app` where you connect your ChatGPT account once.
116
+ That connection installs the authorization in your account's secret
117
+ environment on the instance — shared by the CLI, web console, and macOS app —
118
+ and the CLI itself never sees or stores the OpenAI key. Sama One sessions are
119
+ not limited by the simultaneous-thread lease.
120
+
107
121
  You only do this once per machine. It then offers to **resume** a session tagged for this
108
122
  project + machine, or **start a new one**.
109
123
 
package/dist/index.js CHANGED
@@ -249,6 +249,27 @@ var ApiClient = class {
249
249
  async verify() {
250
250
  return (await this.verifyDetailed()).ok;
251
251
  }
252
+ /**
253
+ * Re-point a thread's ROOT agent (mid-session agent switch). The runtime
254
+ * treats this as an explicit handoff: the full conversation is preserved
255
+ * and the next turn plays on the new agent. Published spec surface
256
+ * (`PATCH /api/threads/:id { agent_id }`), same as every head uses.
257
+ */
258
+ async setThreadAgent(threadId, agentId) {
259
+ await this.json(`/api/threads/${threadId}`, {
260
+ method: "PATCH",
261
+ body: JSON.stringify({ agent_id: agentId })
262
+ });
263
+ }
264
+ /**
265
+ * Whether the account has completed the Sama One (OpenSama) ChatGPT
266
+ * connection. Secret env variables come back as metadata only — the
267
+ * instance never returns the OpenAI key value to a client.
268
+ */
269
+ async openSamaStatus() {
270
+ const body = await this.json("/api/users/me/env");
271
+ return (body.variables ?? []).some((entry) => entry.name === "OPENSAMA_OPENAI_API_KEY");
272
+ }
252
273
  /**
253
274
  * Check the endpoint + token and say PRECISELY what's wrong when they
254
275
  * fail. "That token didn't work" is a lie when the real problem is a typo'd
@@ -5648,7 +5669,7 @@ var Tui = class _Tui {
5648
5669
  */
5649
5670
  select(title, items) {
5650
5671
  return new Promise((resolve) => {
5651
- let idx = 0;
5672
+ let idx = Math.max(0, items.findIndex((it) => !it.disabled));
5652
5673
  this.beginTakeover();
5653
5674
  const cols2 = process.stdout.columns || 80;
5654
5675
  const geo = inputBoxGeometry(cols2);
@@ -5669,7 +5690,7 @@ ${pad}${title}
5669
5690
  let label = it.label.replace(/\s+/g, " ").trim();
5670
5691
  if (label.length > labelMax) label = label.slice(0, Math.max(0, labelMax - 1)) + "\u2026";
5671
5692
  const pointer = sel ? `${C.magenta}\u276F${C.reset} ` : " ";
5672
- const styledLabel = sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
5693
+ const styledLabel = it.disabled ? `${C.gray}${label}${C.reset}` : sel ? `${C.bold}${C.cyan}${label}${C.reset}` : `${C.dim}${label}${C.reset}`;
5673
5694
  let content = `${pointer}${styledLabel}`;
5674
5695
  if (hintW) {
5675
5696
  const used = pointerW + label.length;
@@ -5714,7 +5735,7 @@ ${pad}${title}
5714
5735
  idx = (idx + 1) % items.length;
5715
5736
  draw(true);
5716
5737
  } else if (key.name === "return" || key.name === "enter") {
5717
- close(items[idx].value);
5738
+ if (!items[idx].disabled) close(items[idx].value);
5718
5739
  } else if (key.name === "escape") {
5719
5740
  close(void 0);
5720
5741
  }
@@ -5899,6 +5920,13 @@ var AGENT_ID_VARIANTS = [
5899
5920
  "standard_code_low_agent",
5900
5921
  "standard_code_high_agent"
5901
5922
  ];
5923
+ var OPENSAMA_AGENT_ID = "opensama_agent";
5924
+ var AGENT_CHOICES = [
5925
+ { id: "frontier_one", title: "Frontier One", description: "coming soon", disabled: true },
5926
+ { id: AGENT_ID, title: "Unlimited One", description: "adaptive high/low routing" },
5927
+ { id: OPENSAMA_AGENT_ID, title: "Sama One", description: "ChatGPT Pro sign-in required" },
5928
+ { id: "dario", title: "Dario", description: "unavailable", disabled: true }
5929
+ ];
5902
5930
  var PRODUCTION_ENDPOINT = "https://api.standardcode.ai";
5903
5931
  function readVersion() {
5904
5932
  try {
@@ -6822,7 +6850,7 @@ Run \`standardcode daemon install\` (or plain \`standardcode\`) once to sign in.
6822
6850
  };
6823
6851
  const sweep = async () => {
6824
6852
  try {
6825
- const threads = await api.listThreads(AGENT_ID_VARIANTS, [runnerTag]);
6853
+ const threads = await api.listThreads([...AGENT_ID_VARIANTS, OPENSAMA_AGENT_ID], [runnerTag]);
6826
6854
  const recent = threads.sort((a, b) => (b.created_at ?? 0) - (a.created_at ?? 0)).slice(0, MAX_WORKERS);
6827
6855
  for (const t of recent) {
6828
6856
  await attach(t.id, t.tags, (t.created_at ?? 0) * 1e3);
@@ -7436,6 +7464,9 @@ function printUsage() {
7436
7464
  " If url is omitted, prompt for it.",
7437
7465
  " Credentials are remembered for that endpoint, but",
7438
7466
  " the saved default endpoint is not changed.",
7467
+ " --agent <name> Pick the coding agent without the menu:",
7468
+ " 'unlimited' (Unlimited One, default) or 'sama'",
7469
+ " (Sama One \u2014 requires the ChatGPT connection).",
7439
7470
  " -h, --help Show this help.",
7440
7471
  ""
7441
7472
  ].join("\n")
@@ -7449,6 +7480,18 @@ function parseArgs2(args) {
7449
7480
  parsed.help = true;
7450
7481
  continue;
7451
7482
  }
7483
+ if (arg === "--agent") {
7484
+ const value = args[i + 1];
7485
+ if (!value || value.startsWith("-")) throw new Error("--agent requires a name (unlimited or sama).");
7486
+ parsed.agent = value;
7487
+ i++;
7488
+ continue;
7489
+ }
7490
+ if (arg.startsWith("--agent=")) {
7491
+ parsed.agent = arg.slice("--agent=".length);
7492
+ if (!parsed.agent) throw new Error("--agent requires a name (unlimited or sama).");
7493
+ continue;
7494
+ }
7452
7495
  if (arg === "--endpoint" || arg === "-e") {
7453
7496
  const value = args[i + 1];
7454
7497
  if (value && !value.startsWith("-")) {
@@ -7798,6 +7841,35 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
7798
7841
  void registerProject(api, identity, projectDir).catch(() => {
7799
7842
  });
7800
7843
  const tui = new Tui(1);
7844
+ let selectedAgent = AGENT_ID;
7845
+ if (cliArgs.agent) {
7846
+ const wanted = cliArgs.agent.toLowerCase();
7847
+ if (["sama", "opensama", "sama_one", OPENSAMA_AGENT_ID].includes(wanted)) {
7848
+ selectedAgent = OPENSAMA_AGENT_ID;
7849
+ } else if (["unlimited", "standard", "unlimited_one", AGENT_ID].includes(wanted)) {
7850
+ selectedAgent = AGENT_ID;
7851
+ } else {
7852
+ stdout.write(`${c3.red}error:${c3.reset} Unknown agent "${cliArgs.agent}". Use unlimited or sama.
7853
+ `);
7854
+ process.exit(1);
7855
+ }
7856
+ } else {
7857
+ const picked = await tui.select(
7858
+ `${c3.bold}${gradientText("Which agent?")}${c3.reset} ${c3.dim}\u2191\u2193 \xB7 enter \xB7 esc${c3.reset}`,
7859
+ AGENT_CHOICES.map((choice) => ({
7860
+ label: choice.title,
7861
+ hint: choice.description,
7862
+ value: choice.id,
7863
+ disabled: choice.disabled
7864
+ }))
7865
+ );
7866
+ if (!picked) process.exit(0);
7867
+ selectedAgent = picked;
7868
+ }
7869
+ if (selectedAgent === OPENSAMA_AGENT_ID) {
7870
+ const ok = await ensureSamaAuth(tui, api);
7871
+ if (!ok) process.exit(0);
7872
+ }
7801
7873
  const home = os7.homedir();
7802
7874
  const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
7803
7875
  const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
@@ -7846,14 +7918,17 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
7846
7918
  const loadingSessions = startLoader("Loading sessions");
7847
7919
  let existing = [];
7848
7920
  try {
7849
- existing = await api.listThreads(AGENT_ID_VARIANTS, resumeTags);
7921
+ existing = await api.listThreads(
7922
+ selectedAgent === OPENSAMA_AGENT_ID ? [OPENSAMA_AGENT_ID] : AGENT_ID_VARIANTS,
7923
+ resumeTags
7924
+ );
7850
7925
  } catch {
7851
7926
  existing = [];
7852
7927
  }
7853
7928
  const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
7854
7929
  loadingSessions.stop();
7855
7930
  const createSessionThread = async () => {
7856
- const id = await api.createThread(AGENT_ID, tags);
7931
+ const id = await api.createThread(selectedAgent, tags);
7857
7932
  if (session.mode === "remote" && session.runner && session.remotePath) {
7858
7933
  await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
7859
7934
  });
@@ -7900,6 +7975,70 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
7900
7975
  function shortenPath(p, max = 38) {
7901
7976
  return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
7902
7977
  }
7978
+ async function ensureSamaAuth(tui, api) {
7979
+ const checking = startLoader("Checking your ChatGPT connection");
7980
+ const already = await api.openSamaStatus().catch(() => false);
7981
+ checking.stop();
7982
+ if (already) return true;
7983
+ const picked = await tui.select(
7984
+ `${c3.bold}${gradientText("Authenticate with ChatGPT")}${c3.reset} ${c3.dim}Sama One runs on OpenAI using your own ChatGPT Pro account${c3.reset}`,
7985
+ [
7986
+ {
7987
+ label: "Continue with ChatGPT",
7988
+ hint: "connect once \u2014 unlocks your whole account",
7989
+ value: "continue"
7990
+ },
7991
+ { label: "Not now", hint: "go back", value: "cancel" }
7992
+ ]
7993
+ );
7994
+ if (picked !== "continue") return false;
7995
+ openUrl("https://standardcode.ai/app?connect=sama");
7996
+ tui.print(
7997
+ `${c3.dim}Finish connecting ChatGPT in the browser \u2014 waiting here for the authorization to land on your account\u2026${c3.reset}`
7998
+ );
7999
+ const waiting = startLoader("Waiting for your ChatGPT authorization");
8000
+ const deadline = Date.now() + 5 * 60 * 1e3;
8001
+ while (Date.now() < deadline) {
8002
+ await new Promise((r) => setTimeout(r, 3e3));
8003
+ if (await api.openSamaStatus().catch(() => false)) {
8004
+ waiting.stop();
8005
+ tui.print(
8006
+ `${c3.green}\u2713${c3.reset} ChatGPT connected \u2014 Sama One is now unlocked on your account (terminal, web, and macOS app).`
8007
+ );
8008
+ return true;
8009
+ }
8010
+ }
8011
+ waiting.stop();
8012
+ tui.print(
8013
+ `${c3.yellow}Still not connected.${c3.reset} Finish the flow at ${c3.teal}standardcode.ai/app${c3.reset} and pick Sama One again.`
8014
+ );
8015
+ return false;
8016
+ }
8017
+ async function runAgentSwitchMenu(tui, api, threadId) {
8018
+ const picked = await tui.select(
8019
+ `${c3.bold}${gradientText("Switch agent")}${c3.reset} ${c3.dim}takes effect on the next message${c3.reset}`,
8020
+ AGENT_CHOICES.map((choice) => ({
8021
+ label: choice.title,
8022
+ hint: choice.description,
8023
+ value: choice.id,
8024
+ disabled: choice.disabled
8025
+ }))
8026
+ );
8027
+ if (!picked) return;
8028
+ if (picked === OPENSAMA_AGENT_ID) {
8029
+ const ok = await ensureSamaAuth(tui, api);
8030
+ if (!ok) return;
8031
+ }
8032
+ const title = AGENT_CHOICES.find((choice) => choice.id === picked)?.title ?? picked;
8033
+ try {
8034
+ await api.setThreadAgent(threadId, picked);
8035
+ tui.print(`${c3.green}\u2713${c3.reset} Session handed to ${c3.bold}${title}${c3.reset} \u2014 applies from your next message.`);
8036
+ } catch (e) {
8037
+ tui.print(
8038
+ `${c3.red}\u2717 couldn't switch agent:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`
8039
+ );
8040
+ }
8041
+ }
7903
8042
  async function pickRemoteProject(tui, api, runner) {
7904
8043
  const ENTER_PATH = "__enter_path__";
7905
8044
  const projects = Object.entries(runner.projects).sort(
@@ -8611,6 +8750,12 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
8611
8750
  run: () => runMcpMenu(tui, mcpCtl)
8612
8751
  }
8613
8752
  ],
8753
+ {
8754
+ name: "agent",
8755
+ label: "Switch agent",
8756
+ hint: "hand this session to a different agent",
8757
+ run: () => runAgentSwitchMenu(tui, api, threadId)
8758
+ },
8614
8759
  {
8615
8760
  name: "machines",
8616
8761
  label: "Your machines",