@standardagents/code 0.9.6 → 0.9.7

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,45 @@ ${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 connecting = startLoader("Checking your ChatGPT connection");
7871
+ const hasKey = await api.openSamaStatus().catch(() => false);
7872
+ connecting.stop();
7873
+ if (!hasKey) {
7874
+ stdout.write(
7875
+ `${c3.yellow}Sama One needs your ChatGPT account connected first.${c3.reset}
7876
+ Opening ${c3.teal}https://standardcode.ai/app${c3.reset} \u2014 connect ChatGPT there, then run standardcode again.
7877
+ `
7878
+ );
7879
+ openUrl("https://standardcode.ai/app");
7880
+ process.exit(0);
7881
+ }
7882
+ }
7801
7883
  const home = os7.homedir();
7802
7884
  const tildeDir = projectDir.startsWith(home) ? "~" + projectDir.slice(home.length) : projectDir;
7803
7885
  const shortDir = tildeDir.length > 38 ? "\u2026" + tildeDir.slice(-37) : tildeDir;
@@ -7846,14 +7928,17 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
7846
7928
  const loadingSessions = startLoader("Loading sessions");
7847
7929
  let existing = [];
7848
7930
  try {
7849
- existing = await api.listThreads(AGENT_ID_VARIANTS, resumeTags);
7931
+ existing = await api.listThreads(
7932
+ selectedAgent === OPENSAMA_AGENT_ID ? [OPENSAMA_AGENT_ID] : AGENT_ID_VARIANTS,
7933
+ resumeTags
7934
+ );
7850
7935
  } catch {
7851
7936
  existing = [];
7852
7937
  }
7853
7938
  const summaries = existing.length > 0 ? await summarizeThreads(api, existing.slice(0, 8)) : [];
7854
7939
  loadingSessions.stop();
7855
7940
  const createSessionThread = async () => {
7856
- const id = await api.createThread(AGENT_ID, tags);
7941
+ const id = await api.createThread(selectedAgent, tags);
7857
7942
  if (session.mode === "remote" && session.runner && session.remotePath) {
7858
7943
  await api.kvSet(id, "session_info", { cwd: session.remotePath, machine: session.runner.name }).catch(() => {
7859
7944
  });
@@ -7900,6 +7985,37 @@ ${c3.dim}Press Control-C again to exit${c3.reset}
7900
7985
  function shortenPath(p, max = 38) {
7901
7986
  return p.length > max ? "\u2026" + p.slice(-(max - 1)) : p;
7902
7987
  }
7988
+ async function runAgentSwitchMenu(tui, api, threadId) {
7989
+ const picked = await tui.select(
7990
+ `${c3.bold}${gradientText("Switch agent")}${c3.reset} ${c3.dim}takes effect on the next message${c3.reset}`,
7991
+ AGENT_CHOICES.map((choice) => ({
7992
+ label: choice.title,
7993
+ hint: choice.description,
7994
+ value: choice.id,
7995
+ disabled: choice.disabled
7996
+ }))
7997
+ );
7998
+ if (!picked) return;
7999
+ if (picked === OPENSAMA_AGENT_ID) {
8000
+ const hasKey = await api.openSamaStatus().catch(() => false);
8001
+ if (!hasKey) {
8002
+ tui.print(
8003
+ `${c3.yellow}Sama One needs your ChatGPT account connected first \u2014 opening ${c3.teal}standardcode.ai/app${c3.reset}${c3.yellow}.${c3.reset}`
8004
+ );
8005
+ openUrl("https://standardcode.ai/app");
8006
+ return;
8007
+ }
8008
+ }
8009
+ const title = AGENT_CHOICES.find((choice) => choice.id === picked)?.title ?? picked;
8010
+ try {
8011
+ await api.setThreadAgent(threadId, picked);
8012
+ tui.print(`${c3.green}\u2713${c3.reset} Session handed to ${c3.bold}${title}${c3.reset} \u2014 applies from your next message.`);
8013
+ } catch (e) {
8014
+ tui.print(
8015
+ `${c3.red}\u2717 couldn't switch agent:${c3.reset} ${c3.gray}${e instanceof Error ? e.message : String(e)}${c3.reset}`
8016
+ );
8017
+ }
8018
+ }
7903
8019
  async function pickRemoteProject(tui, api, runner) {
7904
8020
  const ENTER_PATH = "__enter_path__";
7905
8021
  const projects = Object.entries(runner.projects).sort(
@@ -8611,6 +8727,12 @@ ${c3.gray}Close another session (its slot frees within ~90s), then resend your m
8611
8727
  run: () => runMcpMenu(tui, mcpCtl)
8612
8728
  }
8613
8729
  ],
8730
+ {
8731
+ name: "agent",
8732
+ label: "Switch agent",
8733
+ hint: "hand this session to a different agent",
8734
+ run: () => runAgentSwitchMenu(tui, api, threadId)
8735
+ },
8614
8736
  {
8615
8737
  name: "machines",
8616
8738
  label: "Your machines",