codeply-cli 0.5.3 → 0.5.5

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/lib/agent.mjs CHANGED
@@ -40,7 +40,7 @@ const MAX_MALFORMED_RETRIES = 3;
40
40
  const CONTEXT_CHAR_BUDGET = 64000;
41
41
  function contextBudgetFor(route) {
42
42
  if (route && route.custom) return route.custom.kind === 'ollama' ? 100000 : 200000;
43
- if (route && route.auto) return 160000; // Gemma 4 31B on Ollama Cloud has a large window
43
+ if (route && route.auto) return 160000; // about 40k tokens: keeps Auto's Claude steps cheap (Haiku 5.5 costs 5x past 100k)
44
44
  return CONTEXT_CHAR_BUDGET;
45
45
  }
46
46
 
@@ -52,7 +52,7 @@ const FORMAT_CORRECTION =
52
52
  'closing tags complete:\n\n' +
53
53
  '<codeply:read_file>\n<path>run.html</path>\n</codeply:read_file>\n\n' +
54
54
  'Valid names: todo, ask_user, mcp, list_dir, read_file, write_file, edit_file, search, run, use_skill, list_skills, fetch_image, ' +
55
- 'browser_check, gmail_send, gmail_search, gmail_draft, drafts_list, calendar_list, calendar_add, slack_post_message, vercel_deploy, supabase_create_project, ' +
55
+ 'browser_check, gmail_send, gmail_search, gmail_draft, drafts_list, calendar_list, calendar_add, add_model, slack_post_message, vercel_deploy, supabase_create_project, ' +
56
56
  'supabase_delete_project, github_create_repo, design_reference_search, view_images, supabase_api, supabase_sql, vercel_api, ' +
57
57
  'web_fetch, web_search, image_search, reach_me, watch_email, apply_patch, plan_exit, plan_enter, lsp, publish_check, publish_connect, supabase_setup, supabase_schema, ' +
58
58
  'publish_deploy, publish_github.';
@@ -329,6 +329,7 @@ const PARAMS = {
329
329
  drafts_list: [],
330
330
  calendar_list: ['from', 'to', 'max'],
331
331
  calendar_add: ['title', 'start', 'end', 'description', 'location', 'reminders'],
332
+ add_model: ['name', 'base_url', 'model', 'api_key'],
332
333
  slack_post_message: ['channel', 'text'],
333
334
  vercel_deploy: ['path'],
334
335
  supabase_create_project: ['name'],
@@ -865,6 +866,16 @@ drafts; gmail_send with <draft>id</draft> sends one of them.)
865
866
  <reminders>30</reminders>
866
867
  </codeply:calendar_add>
867
868
 
869
+ <codeply:add_model>
870
+ <name>GPT-5 mini</name>
871
+ <base_url>https://api.openai.com/v1</base_url>
872
+ <model>gpt-5-mini</model>
873
+ <api_key>the key the user gave</api_key>
874
+ </codeply:add_model>
875
+
876
+ (add_model puts a model in the user's model menu when they ask; it asks them
877
+ first, tests it, selects it and syncs it to their phone. Never repeat the key.)
878
+
868
879
  (Calendar times are the user's local time: 2026-10-06T15:30 for a time,
869
880
  2026-10-06 for a whole day; from/to also take today or tomorrow. calendar_list
870
881
  defaults to today. calendar_add puts a real event on the user's Google
@@ -1043,7 +1054,8 @@ RULES
1043
1054
  - Tool results are the only source of truth about what happened. A failed result (non-2xx status, non-zero exit code, "did not apply") means it did not happen - never describe it as done.
1044
1055
  - Creating a remote repo, pushing code, enabling Pages, or deploying anything are exactly the kind of claim covered by "never report success you did not verify" above - and the easiest one to get wrong, because each step's own command can silently no-op or partially fail while a LATER step still appears to succeed. Concretely: run whoami equivalents (gh api user, gh auth status) to get the real signed-in username BEFORE building any URL with it - never guess a username from the OS account name, the folder name, or anything the user said earlier that could be stale; after gh repo create or git push, treat the command's own exit code and printed output as the only source of truth for whether it worked, not your prior turn's summary of what you intended to do - a command you ran two turns ago having succeeded is not evidence this turn's retry did too; and never hand the user a repository/deployment URL you have not just confirmed resolves (curl -I it, or read it back from the command's own output) - a plausible-looking URL built from a guessed username/slug is a fabrication even if the pattern is usually right.
1045
1056
  - gmail_send and slack_post_message send a real email or a real Slack message the moment they run - there is no "preview" mode. Only use them when the user actually asked for that email/message to go out, never speculatively, never as a way to "show" them what it would say. When the user wants an email written but not sent yet, use gmail_draft instead. The user sees the email on an editable card before it goes out and may change it: the tool result says what was really sent or saved, so report that, not your original wording. calendar_add likewise puts a real event on the user's Google Calendar: only when they asked for it, with times in their local time; check calendar_list first when they ask what is on their calendar or whether a time is free. If gmail_search or a prior message makes clear Gmail/Slack isn't connected, say so plainly and stop - do not retry hoping it connects itself, and do not claim you sent something when the tool reported it wasn't connected.
1046
- - vercel_deploy, supabase_create_project, supabase_delete_project, and github_create_repo are the same category as gmail_send/slack_post_message above: real, immediate action the moment they run - a live production deployment, a newly provisioned cloud database with its own bill, a brand-new repository pushed with the user's code. Only use them when the user actually asked for that outcome, never speculatively "to check if it would work." If one reports its integration isn't connected, say so plainly and stop rather than retrying or working around it. supabase_delete_project is the sharpest of these - it permanently destroys a database with no undo - so only reach for it when the user has clearly asked to delete or remove a specific project, never as cleanup for something that merely looks unused.
1057
+ - A brand-new project: when the user asks you to start, create or build a new app or project from scratch (an empty or new folder), then once the first version runs, back it up on GitHub without being asked again: call github_create_repo (it creates a private repo on their connected account and pushes; the user approves it on a card, no env vars, tokens or github.com steps). If it reports GitHub isn't connected, mention once, in one line, that connecting GitHub in Connect Apps backs projects up automatically, and carry on. Don't do this for existing projects, folders that are already git repos with a remote, or small one-off files.
1058
+ - vercel_deploy, supabase_create_project, supabase_delete_project, and github_create_repo are the same category as gmail_send/slack_post_message above: real, immediate action the moment they run - a live production deployment, a newly provisioned cloud database with its own bill, a brand-new repository pushed with the user's code. Only use them when the user actually asked for that outcome (or, for github_create_repo, a brand-new project as described above), never speculatively "to check if it would work." If one reports its integration isn't connected, say so plainly and stop rather than retrying or working around it. supabase_delete_project is the sharpest of these - it permanently destroys a database with no undo - so only reach for it when the user has clearly asked to delete or remove a specific project, never as cleanup for something that merely looks unused.
1047
1059
  - Images: build without photos first. Most pages look great with type, color, layout, CSS shapes, icons and inline SVG, so do that unless the design truly needs a photo (a restaurant's food, a product shot, a portfolio). Never ask the user for images or how imagery should work; decide yourself. When a photo is genuinely needed, find a real one with image_search (it searches the whole web like Google Images), pick the result that fits that exact section, and download it with fetch_image to a path under assets/. Every image in the markup must be that local file: never point an <img> or CSS background at a remote URL, and never use loremflickr, picsum or other random-photo services. If a downloaded image turns out broken or unrelated once you look at the page, delete it and pick another result.
1048
1060
  - If a SKILLS entry below is a clear match for the task, use_skill it before starting - its instructions take priority over your own default approach for that kind of work. Do not use_skill speculatively; only when a listed skill actually matches what you are about to do. A name under LIKELY RELEVANT TO THIS REQUEST, if that section is present, was matched against your actual request from the full library - treat it exactly the same way: use_skill it before starting unless it's obviously a false match, do not silently ignore it in favor of guessing your own approach.
1049
1061
  - EXCEPTION - this one is not speculative: if the task is to build or restyle any page a human will look at in a browser (a landing page, a small-business site, a portfolio, a dashboard, any HTML/CSS/UI), use_skill 'premium-web-design' before writing markup, even if the brief sounds tiny or mundane ("a site for a tea shop"). A plain-sounding brief is not permission for a flat, default-Bootstrap-looking result - Codeply's bar is that every generated page reads as deliberately designed. Skip this only if the user explicitly asked for something minimal/utilitarian/no-frills.
@@ -1429,7 +1441,7 @@ const NO_NATIVE_TOOLS_REMINDER =
1429
1441
  // unless their entry says toolMode: 'text', or CODEPLY_TOOLS=text is set.
1430
1442
  // A model that rejects tools is remembered for the rest of the session.
1431
1443
  const nativeUnsupported = new Set();
1432
- const MUTATING_ACTIONS = new Set(['write_file', 'edit_file', 'apply_patch', 'fetch_image', 'gmail_send', 'gmail_draft', 'calendar_add', 'slack_post_message', 'vercel_deploy', 'supabase_create_project', 'supabase_delete_project', 'github_create_repo', 'supabase_setup', 'supabase_schema', 'publish_deploy', 'publish_github']);
1444
+ const MUTATING_ACTIONS = new Set(['write_file', 'edit_file', 'apply_patch', 'fetch_image', 'gmail_send', 'gmail_draft', 'calendar_add', 'add_model', 'slack_post_message', 'vercel_deploy', 'supabase_create_project', 'supabase_delete_project', 'github_create_repo', 'supabase_setup', 'supabase_schema', 'publish_deploy', 'publish_github']);
1433
1445
 
1434
1446
  /**
1435
1447
  * Native function names for MCP tools (mcp__server__tool, within the 64-char
package/lib/ai.js CHANGED
@@ -289,6 +289,7 @@ async function chatViaProxy(messages, opts) {
289
289
  'Content-Type': 'application/json',
290
290
  'Authorization': `Bearer ${token}`,
291
291
  'apikey': SUPABASE_ANON_KEY,
292
+ ...(deviceId() ? { 'x-codeply-device': deviceId() } : {}),
292
293
  },
293
294
  // meta is optional, display-only context (what was asked, which file) -
294
295
  // the proxy logs it to usage_history so CLI activity shows up in the
@@ -1223,14 +1224,88 @@ async function testModel(m) {
1223
1224
  }
1224
1225
 
1225
1226
  // ─── Auto ───────────────────────────────────────────────────────────────────
1226
- // "Auto" is Gemma 4 31B on Ollama Cloud. Two account keys back it (a release
1227
- // build bakes OLLAMA_API_KEY / OLLAMA_API_KEY_FALLBACK in via
1228
- // scripts/embed-secrets.js; from source they come from ~/.codeply/config.json).
1229
- // chatViaOllama moves to the second key the moment the first is over quota or
1230
- // failing. Only if no key works at all does it fall back to the hosted proxy.
1227
+ // "Auto" is Codeply's router (Codeply-App/supabase/functions/auto): Claude
1228
+ // Sonnet 5.5 for coding and hard work, Claude Haiku 5.5 for chat, images and
1229
+ // background jobs, gpt-oss-120b and Gemma 4 when a daily allowance is used up.
1230
+ // It decides per turn on the server, where the Claude key and the budgets
1231
+ // live. If the router can't be reached at all, Auto falls back to Gemma 4 31B
1232
+ // on Ollama Cloud directly (two account keys: a release build bakes
1233
+ // OLLAMA_API_KEY / OLLAMA_API_KEY_FALLBACK in via scripts/embed-secrets.js;
1234
+ // from source they come from ~/.codeply/config.json), then the hosted proxy.
1235
+ const AUTO_URL = `${SUPABASE_URL}/functions/v1/auto`;
1231
1236
  const AUTO_MODEL = { host: 'https://ollama.com', model: 'gemma4:31b' };
1237
+ // Long Claude turns: the router streams keep-alive spaces, so allow a while
1238
+ const AUTO_TIMEOUT_MS = 6 * 60_000;
1239
+
1240
+ /**
1241
+ * This computer, as a one-way hash, so the router can give one Claude
1242
+ * allowance per computer however many accounts sign in on it. Built from the
1243
+ * OS machine id (Windows MachineGuid, the Mac's hardware UUID, Linux
1244
+ * /etc/machine-id), or the first network card's MAC when that can't be read.
1245
+ * The raw values never leave the PC.
1246
+ */
1247
+ let deviceIdCache = null;
1248
+ function deviceId() {
1249
+ if (deviceIdCache !== null) return deviceIdCache;
1250
+ const os = require('os');
1251
+ const { execSync } = require('child_process');
1252
+ let machine = '';
1253
+ try {
1254
+ if (process.platform === 'win32') {
1255
+ const out = execSync('reg query HKLM\\SOFTWARE\\Microsoft\\Cryptography /v MachineGuid', { encoding: 'utf8', windowsHide: true, timeout: 3000 });
1256
+ machine = (/MachineGuid\s+REG_SZ\s+(\S+)/.exec(out) || [])[1] || '';
1257
+ } else if (process.platform === 'darwin') {
1258
+ const out = execSync('ioreg -rd1 -c IOPlatformExpertDevice', { encoding: 'utf8', timeout: 3000 });
1259
+ machine = (/"IOPlatformUUID" = "([^"]+)"/.exec(out) || [])[1] || '';
1260
+ } else {
1261
+ machine = require('fs').readFileSync('/etc/machine-id', 'utf8').trim();
1262
+ }
1263
+ } catch {}
1264
+ const macs = Object.values(os.networkInterfaces()).flat()
1265
+ .filter((n) => n && !n.internal && n.mac && n.mac !== '00:00:00:00:00:00')
1266
+ .map((n) => n.mac).sort();
1267
+ const raw = machine || macs[0] || '';
1268
+ deviceIdCache = raw ? require('crypto').createHash('sha256').update(`codeply-device|${raw}`).digest('hex') : '';
1269
+ return deviceIdCache;
1270
+ }
1271
+
1272
+ async function chatViaRouter(messages, opts) {
1273
+ const token = await getAccessToken();
1274
+ // A cloud run has no sign-in: its usage key (CODEPLY_TRACK_KEY) stands in for the person
1275
+ const trackKey = token ? '' : String(process.env.CODEPLY_TRACK_KEY || '').trim();
1276
+ const device = trackKey ? '' : deviceId();
1277
+ if (!token && !trackKey) return { success: false, unreachable: true, error: 'Not signed in.' };
1278
+ return withRetries(async () => {
1279
+ let res, body;
1280
+ const { signal, isTimeout, cleanup } = withTimeout(opts.signal, AUTO_TIMEOUT_MS);
1281
+ try {
1282
+ res = await fetch(AUTO_URL, {
1283
+ method: 'POST',
1284
+ headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : { 'x-codeply-track-key': trackKey }), apikey: SUPABASE_ANON_KEY, ...(device ? { 'x-codeply-device': device } : {}) },
1285
+ body: JSON.stringify({
1286
+ messages: wireMessages(messages),
1287
+ ...(opts.tools ? { tools: opts.tools } : {}),
1288
+ opts: { json: !!opts.json, maxTokens: opts.maxTokens, temperature: opts.temperature },
1289
+ }),
1290
+ signal,
1291
+ });
1292
+ body = JSON.parse((await res.text()).trim() || '{}');
1293
+ } catch (e) {
1294
+ if (isAbortError(e) && !isTimeout()) return { aborted: true };
1295
+ return { retryable: true, error: isTimeout() ? 'Auto took too long to answer.' : e.message };
1296
+ } finally {
1297
+ cleanup();
1298
+ }
1299
+ if (body.success) return { done: true, value: { success: true, data: body.data, modelUsed: body.modelUsed, route: body.route } };
1300
+ if (body.error) return { done: true, value: { success: false, error: body.error } };
1301
+ return { retryable: isTransientStatus(res.status), retryAfterMs: retryAfterMs(res), error: describeStatus(res.status) };
1302
+ }, opts.signal);
1303
+ }
1232
1304
 
1233
1305
  async function chatViaAuto(messages, opts) {
1306
+ const routed = await chatViaRouter(messages, opts);
1307
+ if (routed.success || routed.aborted) return routed;
1308
+ // The router itself is unreachable or broken: the old direct route still works.
1234
1309
  const cfg = getConfig();
1235
1310
  const keys = { apiKey: cfg.ollama.apiKey, apiKeyFallback: cfg.ollama.apiKeyFallback };
1236
1311
  if (!keys.apiKey && !keys.apiKeyFallback) return chatViaProxy(messages, opts);
@@ -1260,6 +1335,10 @@ async function chatResearch(messages, opts, rm) {
1260
1335
  const rmod = researchMode();
1261
1336
  const target = rmod.resolveTarget(rm);
1262
1337
  if (target.error) return { success: false, error: target.error };
1338
+ // Online without their own key runs on Codeply's Ollama account: signed-in people only
1339
+ if (rm.mode !== 'local' && !String(rm.apiKey || '').trim() && !(await getAccessToken().catch(() => null))) {
1340
+ return { success: false, error: 'Sign in to Codeply to use Research Mode online, or add your own Ollama key in its settings.' };
1341
+ }
1263
1342
  if (!rm.model) return { success: false, error: 'Pick a model in Research Mode settings.' };
1264
1343
  const pre = rmod.preprocess({ messages, mode: rm.mode, model: rm.model, researchContext: rm.context });
1265
1344
  return chatViaOllamaNative(rmod.withSystem(pre), opts, { baseUrl: target.host, model: rm.model, apiKey: target.apiKey, research: rm.mode === 'cloud' ? 'cloud' : 'local' });
@@ -1267,8 +1346,6 @@ async function chatResearch(messages, opts, rm) {
1267
1346
 
1268
1347
  /** Which route a request takes, for the usage counts (telemetry.js). */
1269
1348
  function providerOf(opts) {
1270
- const rm = getConfig().research;
1271
- if (rm && rm.enabled) return 'research';
1272
1349
  if (opts.route && opts.route.custom) return opts.route.custom.kind === 'chatgpt' ? 'chatgpt' : opts.route.custom.kind === 'ollama' ? 'ollama' : 'byok';
1273
1350
  if (opts.route && opts.route.auto) return 'auto';
1274
1351
  const p = applyRoute(getConfig(), opts.route).provider;
@@ -1290,9 +1367,13 @@ async function chat(messages, opts = {}) {
1290
1367
  }
1291
1368
 
1292
1369
  async function chatRouted(messages, opts = {}) {
1293
- // Research Mode, when switched on in settings, takes every request.
1370
+ // Research Mode: the user's research context goes in front, and whatever
1371
+ // model is selected (Auto or their own) answers as usual.
1294
1372
  const rm = getConfig().research;
1295
- if (rm && rm.enabled) return chatResearch(messages, opts, rm);
1373
+ if (rm && rm.enabled && String(rm.context || '').trim()) {
1374
+ const rmod = researchMode();
1375
+ messages = rmod.withSystem(rmod.preprocess({ messages, researchContext: rm.context }));
1376
+ }
1296
1377
  // A user-added model (desktop model picker) always wins when routed.
1297
1378
  if (opts.route && opts.route.custom) return chatViaCustom(messages, opts, opts.route.custom);
1298
1379
  // Auto in the desktop app = the hosted Codeply model, regardless of what
package/lib/cloud.mjs CHANGED
@@ -104,6 +104,7 @@ jobs:
104
104
  CRAFT_ENGINE: \${{ vars.CRAFT_ENGINE }}
105
105
  CRAFT_ENV: \${{ secrets.CRAFT_ENV }}
106
106
  CRAFT_MERGE: \${{ vars.CRAFT_MERGE }}
107
+ CRAFT_PERMISSIONS: \${{ vars.CRAFT_PERMISSIONS }}
107
108
  CRAFT_BASE: \${{ github.ref_name }}
108
109
  CRAFT_PROMPT: \${{ inputs.prompt }}
109
110
  CRAFT_MODE: \${{ inputs.mode }}
@@ -379,14 +380,18 @@ export async function ensureMirror({ api, cwd, home = defaultHome() }) {
379
380
  throw new Error(`Could not find a free name for the mirror (tried ${base} to ${base}-9).`);
380
381
  }
381
382
 
382
- /** Which model the runner uses. Local models can't be reached from GitHub. */
383
+ /**
384
+ * Which model a cloud run uses. Auto (Codeply's router, through the run's usage
385
+ * key, counted in the person's allowance) unless they picked one of their own
386
+ * models that GitHub can reach; a model on this PC (or with no key) uses Auto.
387
+ */
388
+ export const AUTO_CLOUD = { kind: 'auto', model: 'Auto', sig: 'auto' };
383
389
  export function cloudModelConfig(model) {
384
- if (!model || !model.model) return { error: 'Pick one of your own models first. Cloud runs use your key, not Auto.' };
390
+ if (!model || !model.model) return AUTO_CLOUD;
385
391
  const kind = model.kind === 'ollama' ? 'ollama' : 'openai';
386
392
  const baseUrl = kind === 'ollama' ? String(model.baseUrl || 'https://ollama.com').replace(/\/+$/, '').replace(/\/(api|v1)$/i, '') : String(model.baseUrl || '').trim();
387
- if (!baseUrl) return { error: 'That model has no base URL.' };
388
- if (/^https?:\/\/(localhost|127\.|0\.0\.0\.0|\[::1\]|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(baseUrl)) return { error: `${model.name || model.model} runs on this PC, so GitHub can't reach it. Pick a cloud model for cloud runs.` };
389
- if (!model.apiKey) return { error: `${model.name || model.model} has no API key.` };
393
+ if (!baseUrl || !model.apiKey) return AUTO_CLOUD;
394
+ if (/^https?:\/\/(localhost|127\.|0\.0\.0\.0|\[::1\]|192\.168\.|10\.|172\.(1[6-9]|2\d|3[01])\.)/i.test(baseUrl)) return AUTO_CLOUD;
390
395
  return { kind, baseUrl, model: model.model, apiKey: model.apiKey, sig: crypto.createHash('sha256').update([kind, baseUrl, model.model, model.apiKey].join('\n')).digest('hex').slice(0, 16) };
391
396
  }
392
397
 
@@ -398,7 +403,13 @@ async function setVariable(api, repo, name, value) {
398
403
  /** Put the model key in the mirror's encrypted secrets and the rest in variables. */
399
404
  export async function configureModel({ api, repo, model }) {
400
405
  const cfg = cloudModelConfig(model);
401
- if (cfg.error) throw new Error(cfg.error);
406
+ if (cfg.kind === 'auto') {
407
+ // Auto: no model key in the repo at all; the runner reaches Codeply's router with its usage key
408
+ await api('DELETE', `/repos/${repo}/actions/secrets/CODEPLY_API_KEY`, null, { allow: [404] });
409
+ for (const name of ['CODEPLY_BASE_URL', 'CODEPLY_MODEL']) await api('DELETE', `/repos/${repo}/actions/variables/${name}`, null, { allow: [404] });
410
+ await setVariable(api, repo, 'CODEPLY_MODEL_KIND', 'auto');
411
+ return cfg;
412
+ }
402
413
  const pk = (await api('GET', `/repos/${repo}/actions/secrets/public-key`)).json;
403
414
  await api('PUT', `/repos/${repo}/actions/secrets/CODEPLY_API_KEY`, { encrypted_value: sealSecret(pk.key, cfg.apiKey), key_id: pk.key_id });
404
415
  await setVariable(api, repo, 'CODEPLY_MODEL_KIND', cfg.kind);
@@ -467,8 +478,8 @@ export async function ensureTrackKey({ api, repo, cwd, home = defaultHome(), fet
467
478
  try {
468
479
  const p = getProject(cwd, home) || {};
469
480
  if (p.trackKeyRepo === repo) return false;
470
- if (process.env.CODEPLY_TELEMETRY === '0') return false;
471
- try { if (require('./config.js').getConfig().telemetry === false) return false; } catch {}
481
+ // Needed whatever the usage-stats setting: the run reaches Auto and the connected
482
+ // apps with it (telemetry.js still sends no stats when they're switched off).
472
483
  const auth = require('./auth.js');
473
484
  const token = await auth.getAccessToken().catch(() => null);
474
485
  if (!token) return false;
@@ -549,7 +560,7 @@ export async function loadCloudConnections({ env = process.env, fetchImpl = fetc
549
560
  }
550
561
 
551
562
  // Bumped when CLOUD_WORKFLOW changes, so repos set up earlier get the new file once.
552
- export const WORKFLOW_REV = 2;
563
+ export const WORKFLOW_REV = 3;
553
564
 
554
565
  /**
555
566
  * One-click setup. With `target: 'repo'` (default when the project has a GitHub
@@ -578,7 +589,7 @@ export async function setupCloud({ cwd, token, model, target, envText, home = de
578
589
  }
579
590
  await configureModel({ api, repo, model });
580
591
  if (envText != null) await setProjectEnv({ api, repo, envText });
581
- updateProject(cwd, home, (p) => ({ ...p, modelSig: cfg.sig, modelName: model.name || model.model, enabled: true, workflowRev: WORKFLOW_REV }));
592
+ updateProject(cwd, home, (p) => ({ ...p, modelSig: cfg.sig, modelName: cfg.kind === 'auto' ? 'Auto' : (model.name || model.model), enabled: true, workflowRev: WORKFLOW_REV }));
582
593
  await ensureTrackKey({ api, repo, cwd, home, fetchImpl });
583
594
  await syncCloudConnections({ home, fetchImpl });
584
595
  return { repo, created, kind, base, url: `${(serverUrl || 'https://github.com').replace(/\/$/, '')}/${repo}`, skipped };
@@ -629,12 +640,12 @@ export async function startCloudRun({ cwd, token, prompt, mode = 'Build', sessio
629
640
  if (prompt.length > MAX_PROMPT) throw new Error(`That task is too long for a cloud run (${prompt.length} characters, the limit is ${MAX_PROMPT}).`);
630
641
  mode = ['Build', 'Plan', 'Ask'].includes(mode) ? mode : 'Build';
631
642
  const api = githubApi({ token, apiUrl, fetchImpl });
632
- if (model) {
643
+ // model: undefined leaves the repo's model as it is; null (Auto selected) or a model sets it
644
+ if (model !== undefined) {
633
645
  const cfg = cloudModelConfig(model);
634
- if (cfg.error) throw new Error(cfg.error);
635
646
  if (cfg.sig !== p.modelSig) {
636
647
  await configureModel({ api, repo: p.repo, model });
637
- updateProject(cwd, home, (q) => ({ ...q, modelSig: cfg.sig, modelName: model.name || model.model }));
648
+ updateProject(cwd, home, (q) => ({ ...q, modelSig: cfg.sig, modelName: cfg.kind === 'auto' ? 'Auto' : (model.name || model.model) }));
638
649
  }
639
650
  }
640
651
  const base = p.base || 'main';
@@ -681,6 +692,28 @@ export async function readTaskDoc(api, repo, taskId) {
681
692
  }
682
693
 
683
694
  /** Where a cloud task is: its status file first, then the workflow run (for starts and failed starts). */
695
+ /** The person's answer to a cloud run's question (allow or not), picked up by the runner within seconds. */
696
+ export async function answerCloudAsk({ cwd, token, taskId, askId, approve, home = defaultHome(), apiUrl, fetchImpl }) {
697
+ const task = getTask(cwd, taskId, home);
698
+ if (!task) throw new Error('Unknown cloud task.');
699
+ if (!/^[\w.-]+$/.test(String(askId || ''))) throw new Error('Unknown question.');
700
+ const api = githubApi({ token, apiUrl, fetchImpl });
701
+ const ok = await branchFiles(api, task.repo).write(SESSION_PATHS.answer(askId), { verdict: approve ? 'approve' : 'reject', at: Date.now() }, `Craft answer ${askId}`);
702
+ if (!ok) throw new Error('Could not send your answer to GitHub. Try again.');
703
+ saveTask(cwd, home, { ...task, ask: null });
704
+ return true;
705
+ }
706
+
707
+ /** "ask" (risky actions wait for an OK) or "full" (they just run), kept as the repo variable CRAFT_PERMISSIONS. */
708
+ export async function setCloudPermission({ cwd, token, permission, home = defaultHome(), apiUrl, fetchImpl }) {
709
+ const p = getProject(cwd, home);
710
+ if (!p || !p.repo) throw new Error('Set up cloud runs for this project first.');
711
+ const value = permission === 'full' ? 'full' : 'ask';
712
+ await setVariable(githubApi({ token, apiUrl, fetchImpl }), p.repo, 'CRAFT_PERMISSIONS', value);
713
+ updateProject(cwd, home, (q) => ({ ...q, permission: value }));
714
+ return value;
715
+ }
716
+
684
717
  export async function cloudRunStatus({ cwd, taskId, token, home = defaultHome(), apiUrl, fetchImpl, retryMs = 3000 }) {
685
718
  const task = getTask(cwd, taskId, home);
686
719
  if (!task) throw new Error('Unknown cloud task.');
@@ -695,6 +728,8 @@ export async function cloudRunStatus({ cwd, taskId, token, home = defaultHome(),
695
728
  const { id: _i, startedAt: _s, updatedAt: _u, runId: _r, ...result } = rest;
696
729
  Object.assign(next, { status, result, error: result.error || null, finishedAt: Date.now(), queued: false });
697
730
  } else next.status = 'running';
731
+ // a risky action waiting for the person's OK (answerCloudAsk)
732
+ next.ask = next.status === 'running' && doc.ask && doc.ask.id ? doc.ask : null;
698
733
  saveTask(cwd, home, next);
699
734
  return { ...next, events };
700
735
  }
@@ -950,6 +985,8 @@ export const SESSION_PATHS = {
950
985
  live: (sid) => `live/${sid}.json`,
951
986
  queue: (sid) => `queue/${sid}`,
952
987
  shot: (id, n) => `shots/${id}/${n}.png`,
988
+ // A risky action waiting for the person: Craft or the phone writes the answer here.
989
+ answer: (askId) => `answers/${askId}.json`,
953
990
  // What does not fit in the workflow's inputs (a bot's prompt), written before the dispatch.
954
991
  request: (id) => `requests/${id}.json`,
955
992
  };
@@ -1121,7 +1158,8 @@ async function runOneTask({ task, env, cwd, api, files, repo, token, serverUrl,
1121
1158
  output: { title: 'Starting', summary: 'Starting...' },
1122
1159
  }).catch(() => ({ json: null }))).json : null;
1123
1160
 
1124
- const statusDoc = (status, extra = {}) => ({ id: taskId, status, mode, prompt: prompt.slice(0, 4000), sessionId, ...(kind === 'task' ? { kind } : {}), ...(bot ? { bot: { name: bot.name } } : {}), startedAt, updatedAt: Date.now(), runId: env.GITHUB_RUN_ID || null, ...extra });
1161
+ let pendingAsk = null; let asks = 0;
1162
+ const statusDoc = (status, extra = {}) => ({ ...(pendingAsk && status === 'running' ? { ask: pendingAsk } : {}), id: taskId, status, mode, prompt: prompt.slice(0, 4000), sessionId, ...(kind === 'task' ? { kind } : {}), ...(bot ? { bot: { name: bot.name } } : {}), startedAt, updatedAt: Date.now(), runId: env.GITHUB_RUN_ID || null, ...extra });
1125
1163
  const finish = async (ok, title, summary, result) => {
1126
1164
  const baseLen = JSON.stringify(result).length;
1127
1165
  const packed = packEvents(events, Math.max(4000, MAX_OUTPUT - baseLen - 200));
@@ -1169,7 +1207,31 @@ async function runOneTask({ task, env, cwd, api, files, repo, token, serverUrl,
1169
1207
  const taskBase = await git(cwd, ['rev-parse', 'HEAD']).catch(() => headSha);
1170
1208
  const hist = await loadHistory(files, sessionId);
1171
1209
  await pushProgress(hist.messages.length ? `Continuing the chat (${hist.messages.length / 2} earlier turns).` : 'Working on it.', true);
1172
- const approve = ciApprove(cwd);
1210
+ // Risky actions (deploys...) ask the person unless the repo's Cloud permission is "full".
1211
+ const ASK_WAIT_MS = 15 * 60 * 1000;
1212
+ // Only a repo with the current workflow passes CRAFT_PERMISSIONS (empty when unset): its
1213
+ // Craft and phone can show the question. An older setup keeps the old rule (no) until updated.
1214
+ const rules = ciApprove(cwd, { permission: env.CRAFT_PERMISSIONS, canAsk: env.CRAFT_PERMISSIONS !== undefined });
1215
+ const approve = async (req) => {
1216
+ const v = await rules(req);
1217
+ if (v !== 'ask') return v;
1218
+ const askId = `${taskId}-${++asks}`;
1219
+ pendingAsk = { id: askId, tool: req.tool, title: String(req.title || req.tool).slice(0, 300), detail: String(req.detail || '').slice(0, 2000), at: Date.now() };
1220
+ events.push({ t: 'notice', level: 'info', text: `Waiting for your OK: ${pendingAsk.title}` });
1221
+ await pushProgress('Waiting for your OK.', true);
1222
+ const until = Date.now() + ASK_WAIT_MS;
1223
+ let verdict = null;
1224
+ while (Date.now() < until && !verdict) {
1225
+ await sleep(4000);
1226
+ const f = await files.read(SESSION_PATHS.answer(askId)).catch(() => null);
1227
+ if (f && f.json) { verdict = f.json.verdict === 'approve' ? 'once' : 'reject'; files.remove(SESSION_PATHS.answer(askId), f.sha).catch(() => {}); }
1228
+ else if (heartbeat) heartbeat(true).catch(() => {});
1229
+ }
1230
+ pendingAsk = null;
1231
+ events.push({ t: 'notice', level: 'info', text: verdict === 'once' ? 'Approved.' : verdict === 'reject' ? 'You said no, so it was skipped.' : 'No answer in 15 minutes, so it was skipped.' });
1232
+ await pushProgress('Working.', true);
1233
+ return verdict || 'reject';
1234
+ };
1173
1235
  let answer = ''; let steps = 0; let failure = '';
1174
1236
  const userMessage = kind === 'task'
1175
1237
  ? `${prompt}\n\n(You are running unattended in Codeply Cloud on a Linux machine, not on the user's PC: you cannot reach their local files or desktop apps. Use the shell, the web and this repository. Nobody can answer questions mid-run, so make sensible choices and say what you assumed. Your final reply is read out to the user: say plainly what you did or found, in a few short sentences, no markdown. Do not commit or push; Craft does that when you finish.)`
package/lib/config.js CHANGED
@@ -102,9 +102,9 @@ function getConfig() {
102
102
  const fr = file.research || {};
103
103
  const research = {
104
104
  enabled: fr.enabled === true,
105
- mode: fr.mode === 'cloud' ? 'cloud' : 'local',
105
+ mode: fr.mode === 'local' ? 'local' : 'cloud', // online by default: works with no Ollama installed
106
106
  model: String(fr.model || ''),
107
- apiKey: process.env.OLLAMA_API_KEY || fr.apiKey || '',
107
+ apiKey: fr.apiKey || '', // only a key the person added; without one, research-mode/ollama.js uses Codeply's
108
108
  context: String(fr.context || ''),
109
109
  };
110
110
 
@@ -42,6 +42,12 @@ export const PROVIDERS = {
42
42
 
43
43
  /** Model route from CODEPLY_PROVIDER / CODEPLY_BASE_URL / CODEPLY_MODEL / CODEPLY_API_KEY. */
44
44
  export function routeFromEnv(env) {
45
+ const wantsAuto = String(env.CODEPLY_MODEL_KIND || '').toLowerCase() === 'auto';
46
+ const hasKey = !!String(env.CODEPLY_API_KEY || '').trim();
47
+ if (wantsAuto || (!hasKey && !env.CODEPLY_BASE_URL && env.CODEPLY_TRACK_KEY)) {
48
+ if (!env.CODEPLY_TRACK_KEY) return { error: 'This repo has no Codeply usage key yet. Set cloud up again from Codeply.' };
49
+ return { route: { auto: true } };
50
+ }
45
51
  const provider = String(env.CODEPLY_PROVIDER || 'openai').toLowerCase();
46
52
  const preset = PROVIDERS[provider];
47
53
  const baseUrl = env.CODEPLY_BASE_URL || (preset && preset.baseUrl);
@@ -121,12 +127,21 @@ export function git(cwd, args, { auth, input } = {}) {
121
127
  const slug = (s) => String(s || '').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 30) || 'change';
122
128
  export const clip = (s) => (s.length > MAX_COMMENT ? `${s.slice(0, MAX_COMMENT)}\n\n[... cut, the run log has the rest]` : s);
123
129
 
124
- /** The approver for an unattended run: rules file first, then no pushing, no dangerous commands. */
125
- export function ciApprove(cwd) {
130
+ /**
131
+ * The approver for a run nobody is watching live: the rules file first, never a
132
+ * git push (Craft pushes when the run ends), then the risky actions (deploys,
133
+ * deleting a database, risky commands). Those depend on the repo's Cloud
134
+ * permission (CRAFT_PERMISSIONS): "full" runs them, anything else answers
135
+ * 'ask' so the cloud runner can ask the person in Craft or on the phone. With
136
+ * no way to ask (the /codeply comment agent), 'ask' means no.
137
+ */
138
+ export function ciApprove(cwd, { permission = '', canAsk = false } = {}) {
139
+ const full = String(permission || '').toLowerCase() === 'full';
126
140
  return async (req) => {
127
141
  const d = permissions.decide(req, cwd);
128
- if (d.decision === 'deny' || req.danger || req.tool === 'fetch_image') return 'reject';
142
+ if (d.decision === 'deny' || req.tool === 'fetch_image') return 'reject';
129
143
  if (req.tool === 'run' && /(^|[\s;&|(])(git\b[^;&|\n]*\spush|gh)(\s|$)/.test(`${req.detail || ''}\n${req.title || ''}`)) return 'reject';
144
+ if (req.danger) return full ? 'once' : canAsk ? 'ask' : 'reject';
130
145
  return 'once';
131
146
  };
132
147
  }
@@ -38,6 +38,7 @@ export const TOOL_DOCS = {
38
38
  drafts_list: { d: 'List the drafts saved in Codeply (ids, subject, recipient). Send one with gmail_send and its id as draft.', req: [], p: {} },
39
39
  calendar_list: { d: 'List events on the user\'s Google Calendar in a time range (default: today). Times are local.', req: [], p: { from: 'Start: today, tomorrow, 2026-10-03 or 2026-10-03T14:00. Default today.', to: 'End (a date means through that whole day). Default: end of the from day.', max: 'Most events to return (1-50, default 20).' } },
40
40
  calendar_add: { d: 'Add an event to the user\'s Google Calendar (asks the user first).', req: ['title', 'start'], p: { title: 'Event title.', start: 'Local start: 2026-10-03T14:00, or 2026-10-03 for an all-day event.', end: 'Local end. Default: one hour after start (all-day: the same day).', description: 'Notes for the event.', location: 'Where it is.', reminders: 'Popup reminders, minutes before, comma-separated, e.g. "10,60".' } },
41
+ add_model: { d: 'Add a model to the user\'s Codeply model menu when they ask (they give a name, base URL, model id and API key). Asks them first, tests it, saves it on this computer, selects it and syncs it to their phone. Any OpenAI-compatible API works; Ollama on this computer is http://localhost:11434 with no key.', req: ['base_url', 'model'], p: { name: 'Display name, e.g. "GPT-5 mini". Default: the model id.', base_url: 'OpenAI-compatible base URL, e.g. https://api.openai.com/v1, https://api.deepseek.com, https://openrouter.ai/api/v1.', model: 'The model id the provider expects, e.g. gpt-5-mini, deepseek-chat.', api_key: 'The API key the user gave (leave empty for a server with no key).' } },
41
42
  slack_post_message: { d: 'Post a real Slack message now. Only when the user asked for it.', req: ['channel', 'text'], p: { channel: 'Channel name or id.', text: 'Message text.' } },
42
43
  vercel_deploy: { d: 'Deploy a folder to the connected Vercel account (live, real).', req: [], p: { path: 'Folder to deploy. Default ".".' } },
43
44
  supabase_create_project: { d: 'Create a new Supabase project (billable, real).', req: ['name'], p: { name: 'Project name.' } },
package/lib/telemetry.js CHANGED
@@ -47,7 +47,10 @@ function push(ev) {
47
47
  }
48
48
 
49
49
  /** One model call: { model, provider, tokens_in, tokens_out, ms }. */
50
- function recordAi(ev) { push({ kind: 'ai', ...ev }); }
50
+ function recordAi(ev) {
51
+ if (ev && ev.model === 'fake') return; // the engine tests' stand-in model is not real use
52
+ push({ kind: 'ai', ...ev });
53
+ }
51
54
 
52
55
  /** The app was opened (counts active users even on a day with no AI call), once an hour at most. */
53
56
  const lastOpen = new Map();
package/lib/tools.mjs CHANGED
@@ -1335,13 +1335,23 @@ async function github_create_repo(args, ctx) {
1335
1335
  // at least one commit - a brand-new GitHub repo (created with no
1336
1336
  // auto_init below) starts completely empty, so pushing an empty local
1337
1337
  // repo would just fail with "src refspec main does not match any".
1338
+ try { await runGit('--version', root); } catch {
1339
+ return { ok: false, output: 'Git is not installed on this computer, so the code can\'t be pushed. Tell the user to install it from git-scm.com (the defaults are fine), then ask again.' };
1340
+ }
1338
1341
  const isRepo = fs.existsSync(path.join(root, '.git'));
1339
1342
  if (!isRepo) await runGit('init', root);
1340
1343
  let hasCommit = true;
1341
1344
  try { await runGit('rev-parse HEAD', root); } catch { hasCommit = false; }
1342
1345
  if (!hasCommit) {
1343
1346
  await runGit('add -A', root);
1344
- await runGit('commit -m "Initial commit"', root);
1347
+ // A computer where git was never told who you are would fail here
1348
+ // ("Please tell me who you are"): commit as the GitHub account, with
1349
+ // GitHub's private no-reply address. The git settings stay untouched.
1350
+ const login = String(github.userName || 'codeply').replace(/[^A-Za-z0-9-]/g, '') || 'codeply';
1351
+ let who = '';
1352
+ try { await runGit('config user.name', root); } catch { who += `-c user.name="${login}" `; }
1353
+ try { await runGit('config user.email', root); } catch { who += `-c user.email="${login}@users.noreply.github.com" `; }
1354
+ await runGit(`${who}commit -m "Initial commit"`, root);
1345
1355
  }
1346
1356
 
1347
1357
  const repo = await oauth.githubCreateRepo(github.accessToken, repoName, { private: true });
@@ -2237,6 +2247,48 @@ const publishTools = publishLib.makePublishTools({
2237
2247
  resolveDir: (p, cwd) => resolvePath(String(p).trim(), cwd).abs,
2238
2248
  });
2239
2249
 
2250
+ /**
2251
+ * add_model: adds a model to the user's model menu from chat ("add this model:
2252
+ * name, base URL, API key"). Asks first (key masked), checks the model really
2253
+ * answers, saves it on this computer and selects it. In Craft the desktop does
2254
+ * the saving (globalThis.__codeplyHost.addModel), so it also syncs to the
2255
+ * phone like any added model and the menu updates right away.
2256
+ */
2257
+ async function add_model(args, ctx) {
2258
+ const name = String(args.name || '').trim();
2259
+ const baseUrl = String(args.base_url || args.baseUrl || '').trim().replace(/\/+$/, '');
2260
+ const model = String(args.model || '').trim();
2261
+ const apiKey = String(args.api_key || args.apiKey || '').trim();
2262
+ if (!model) return { ok: false, output: 'add_model needs <model>: the model id the provider expects (e.g. gpt-4o-mini, claude-sonnet-5-5, deepseek-chat).' };
2263
+ if (!/^https?:\/\//i.test(baseUrl)) return { ok: false, output: 'add_model needs <base_url> starting with http:// or https:// (e.g. https://api.openai.com/v1).' };
2264
+ const local = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i.test(baseUrl) && /:11434$/.test(baseUrl);
2265
+ const shown = name || model;
2266
+ const masked = apiKey ? `${apiKey.slice(0, 4)}…${apiKey.slice(-4)}` : 'none';
2267
+ const verdict = readVerdict(await ctx.approve({
2268
+ tool: 'add_model',
2269
+ title: `Add "${shown}" to your models`,
2270
+ detail: `Model: ${model}\nAddress: ${baseUrl}\nKey: ${masked}\n\nIt's tested first, saved on this computer, and synced to your phone.`,
2271
+ danger: false,
2272
+ }));
2273
+ if (verdict.action === 'reject') return { ok: false, output: `User declined to add "${shown}".`, meta: { rejected: true } };
2274
+ const input = { kind: local ? 'ollama' : 'openai', name: shown, baseUrl, model, apiKey };
2275
+ try {
2276
+ const host = globalThis.__codeplyHost;
2277
+ let r;
2278
+ if (host && typeof host.addModel === 'function') r = await host.addModel(input);
2279
+ else {
2280
+ const t = await require('./ai.js').testModel(input);
2281
+ if (!t.ok) return { ok: false, output: `The model didn't answer, so it wasn't added: ${t.error}` };
2282
+ r = config.saveModel({ ...input, phone: true });
2283
+ if (r.ok) config.selectModel(r.model.id);
2284
+ }
2285
+ if (!r || !r.ok) return { ok: false, output: `Couldn't add it: ${(r && r.error) || 'unknown error'}` };
2286
+ return { ok: true, output: `Added "${shown}" (${model} at ${baseUrl}) and selected it. It answered a test request${local ? '' : ' and syncs to the phone'}. Never repeat the API key back to the user.`, meta: { label: shown } };
2287
+ } catch (e) {
2288
+ return { ok: false, output: `Couldn't add it: ${e.message}` };
2289
+ }
2290
+ }
2291
+
2240
2292
  export const TOOLS = {
2241
2293
  ...publishTools,
2242
2294
  todo,
@@ -2246,14 +2298,14 @@ export const TOOLS = {
2246
2298
  web_fetch, web_search, image_search, apply_patch, plan_exit, plan_enter, lsp, reach_me, watch_email,
2247
2299
  list_dir, read_file, write_file, edit_file, search, run, use_skill, list_skills, fetch_image, browser_check,
2248
2300
  gmail_send, gmail_search, gmail_draft, drafts_list, calendar_list, calendar_add, slack_post_message, vercel_deploy, supabase_create_project, supabase_delete_project, github_create_repo,
2249
- design_reference_search, view_images, supabase_api, supabase_sql, vercel_api,
2301
+ design_reference_search, view_images, supabase_api, supabase_sql, vercel_api, add_model,
2250
2302
  };
2251
2303
 
2252
2304
  // Tools that can change something. supabase_api/supabase_sql/vercel_api only
2253
2305
  // actually prompt for non-read requests (see above); read-only tools
2254
2306
  // (browser_check, gmail_search, design_reference_search, view_images, ...)
2255
2307
  // never prompt.
2256
- export const TOOL_NEEDS_APPROVAL = new Set(['supabase_setup', 'supabase_schema', 'publish_deploy', 'publish_github', 'write_file', 'edit_file', 'apply_patch', 'run', 'fetch_image', 'gmail_send', 'gmail_draft', 'calendar_add', 'slack_post_message', 'vercel_deploy', 'supabase_create_project', 'supabase_delete_project', 'github_create_repo', 'supabase_api', 'supabase_sql', 'vercel_api']);
2308
+ export const TOOL_NEEDS_APPROVAL = new Set(['add_model', 'supabase_setup', 'supabase_schema', 'publish_deploy', 'publish_github', 'write_file', 'edit_file', 'apply_patch', 'run', 'fetch_image', 'gmail_send', 'gmail_draft', 'calendar_add', 'slack_post_message', 'vercel_deploy', 'supabase_create_project', 'supabase_delete_project', 'github_create_repo', 'supabase_api', 'supabase_sql', 'vercel_api']);
2257
2309
 
2258
2310
  /** Human-facing verb + colour hint for the transcript. */
2259
2311
  export const TOOL_DISPLAY = {
@@ -2286,6 +2338,7 @@ export const TOOL_DISPLAY = {
2286
2338
  drafts_list:{ verb: 'drafts', icon: '✉' },
2287
2339
  calendar_list:{ verb: 'calendar', icon: '▦' },
2288
2340
  calendar_add:{ verb: 'calendar', icon: '▦' },
2341
+ add_model:{ verb: 'model', icon: '◆' },
2289
2342
  slack_post_message:{ verb: 'post', icon: '#' },
2290
2343
  vercel_deploy:{ verb: 'deploy', icon: '▲' },
2291
2344
  supabase_create_project:{ verb: 'provision', icon: '◆' },
@@ -2308,7 +2361,7 @@ export const TOOL_DISPLAY = {
2308
2361
  // agent.mjs; this covers every other tool that changes something outside the
2309
2362
  // conversation, so read-only can't be escaped through a side door.
2310
2363
  const MUTATING_TOOLS = new Set([
2311
- 'write_file', 'edit_file', 'apply_patch', 'fetch_image', 'gmail_send', 'gmail_draft', 'calendar_add', 'slack_post_message',
2364
+ 'write_file', 'edit_file', 'apply_patch', 'fetch_image', 'gmail_send', 'gmail_draft', 'calendar_add', 'slack_post_message', 'add_model',
2312
2365
  'vercel_deploy', 'supabase_create_project', 'supabase_delete_project', 'github_create_repo',
2313
2366
  'supabase_setup', 'supabase_schema', 'publish_deploy', 'publish_github',
2314
2367
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeply-cli",
3
- "version": "0.5.3",
3
+ "version": "0.5.5",
4
4
  "description": "Codeply from the terminal: an interactive AI coding agent, a local server (codeply serve), and a GitHub agent that runs in your own Actions runner.",
5
5
  "main": "bin/codeply.js",
6
6
  "bin": {