@tenderprompt/cli 0.1.15 → 0.1.16

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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/index.js +701 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -56,7 +56,7 @@ tender capabilities --json
56
56
 
57
57
  tender auth login --device --profile edit-preview --artifact artifact_123 --ttl 7d
58
58
 
59
- tender app init artifact_123 --dir ./widget --json
59
+ tender app init artifact_123 --dir ./widget --preview --json
60
60
 
61
61
  cd ./widget
62
62
  npm run check
@@ -70,7 +70,7 @@ tender app publish artifact_123 --commit "$(git rev-parse HEAD)" --confirm publi
70
70
  For agents, keep the pattern simple:
71
71
 
72
72
  1. start with `tender capabilities --json`
73
- 2. use `tender app init` to clone/bootstrap an existing app checkout
73
+ 2. use `tender app init --preview` to clone/bootstrap an existing app checkout
74
74
  3. inspect and edit files locally
75
75
  4. run `tender app doctor` and local project checks
76
76
  5. push source with Git
@@ -128,8 +128,9 @@ tender --version
128
128
  tender capabilities --json
129
129
  tender app list --json
130
130
  tender app create --name "Exit Intent Widget" --json
131
+ tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json
131
132
  tender app update artifact_123 --name "Sale Widget" --json
132
- tender app init artifact_123 --dir ./widget --json
133
+ tender app init artifact_123 --dir ./widget --preview --json
133
134
  tender app bindings outbound --id shopify --host admin.shopify.com --dry-run --json
134
135
  tender app context status artifact_123 --dir ./widget --json
135
136
  tender app doctor --dir ./widget --json
package/dist/index.js CHANGED
@@ -11,7 +11,7 @@ const CLI_PACKAGE_NAME = "@tenderprompt/cli";
11
11
  const UNKNOWN_CLI_VERSION = "0.0.0";
12
12
  const RECOMMENDED_TENDER_PROMPT_SKILL = {
13
13
  name: "tender-prompt",
14
- version: "0.1.1",
14
+ version: "0.1.2",
15
15
  source: "git@github.com:tenderprompt/skills.git",
16
16
  path: "skills/tender-prompt/SKILL.md",
17
17
  updateHint: "Refresh the tender-prompt skill from git@github.com:tenderprompt/skills.git when the local skill version is older.",
@@ -136,8 +136,9 @@ Examples:
136
136
  tender auth status --json
137
137
  tender app list --json
138
138
  tender app create --name "Exit Intent Widget" --json
139
+ tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json
139
140
  tender app update artifact_123 --name "Sale Widget" --json
140
- tender app init artifact_123 --dir ./widget --json
141
+ tender app init artifact_123 --dir ./widget --preview --json
141
142
  tender app validate artifact_123 --json
142
143
  tender app build artifact_123 --commit $(git rev-parse HEAD) --json
143
144
  tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
@@ -208,7 +209,7 @@ function appHelp() {
208
209
  Commands:
209
210
  tender app list List Tender App records visible to the current token
210
211
  tender app create --name <name>
211
- Create a new Tender App in the current account
212
+ Create a new Tender App, optionally initialize source, and preview
212
213
  tender app update <app-id> --name <name>
213
214
  Update app metadata
214
215
  tender app init <app-id>
@@ -223,6 +224,8 @@ Commands:
223
224
  Show preview job status by latest, commit, or job id
224
225
  tender app preview watch <app-id>
225
226
  Replay lifecycle events by latest, commit, or job id
227
+ tender app agent heartbeat <app-id>
228
+ Send a concise agent status update to the workspace
226
229
  tender app publish <app-id>
227
230
  Publish after dry-run or explicit confirmation
228
231
  tender app publish status <app-id>
@@ -258,7 +261,7 @@ Commands:
258
261
  tender app db query <app-id> --env <preview|published> --sql <sql>
259
262
  Run one read-only app database query
260
263
  tender app context fetch <app-id>
261
- Fetch AGENTS.md, skills, and Tender App scaffold files
264
+ Fetch AGENTS.md, skills, and Tender project context
262
265
  tender app context status <app-id>
263
266
  Show managed context freshness for a checkout
264
267
  tender app context refresh <app-id>
@@ -276,13 +279,15 @@ Commands:
276
279
  Examples:
277
280
  tender app list --json
278
281
  tender app create --name "Exit Intent Widget" --json
282
+ tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json
279
283
  tender app update artifact_123 --name "Sale Widget" --json
280
- tender app init artifact_123 --dir ./widget --json
284
+ tender app init artifact_123 --dir ./widget --preview --json
281
285
  tender app validate artifact_123 --json
282
286
  tender app build artifact_123 --commit $(git rev-parse HEAD) --json
283
287
  tender app preview artifact_123 --commit $(git rev-parse HEAD) --stream --json
284
288
  tender app preview status artifact_123 --commit $(git rev-parse HEAD) --json
285
289
  tender app preview watch artifact_123 --job job_123 --stream --json
290
+ tender app agent heartbeat artifact_123 --source codex --status working --phase editing --summary "Updating the result screen" --json
286
291
  tender app publish artifact_123 --dry-run --json
287
292
  tender app publish status artifact_123 --job job_123 --json
288
293
  tender app publish watch artifact_123 --job job_123 --stream --json
@@ -561,10 +566,18 @@ Examples:
561
566
  TENDER_API_TOKEN=tpat_... tender app list --base-url https://app.tenderprompt.com --json`;
562
567
  }
563
568
  function artifactsCreateHelp() {
564
- return `Usage: tender app create --name <name> [--target-pack <id>] [--target-pack-version <version>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
569
+ return `Usage: tender app create --name <name> [--init] [--dir <path>] [--scaffold <server-backed|none>] [--preview] [--target-pack <id>] [--target-pack-version <version>] [--base-url <url>] [--token <token>] [--profile <name>] [--json]
565
570
 
566
571
  Options:
567
572
  --name <name> App name.
573
+ --init Initialize a local checkout after creating the app.
574
+ --dir <path> Local checkout directory for --init. Defaults to current directory.
575
+ --scaffold <value> API-derived scaffold for empty app source: server-backed or none. Defaults to server-backed.
576
+ --no-scaffold Alias for --scaffold none.
577
+ --preview After --init writes and pushes initial source, request a preview build.
578
+ --remote <name> Git remote name for --init. Defaults to tender.
579
+ --ttl <value> Deprecated for Tender-hosted Git remotes; accepted for compatibility.
580
+ --force Overwrite conflicting managed files during --init.
568
581
  --target-pack <id> Target pack id. Defaults to the API default.
569
582
  --target-pack-version <value> Target pack version. Defaults to the API default.
570
583
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
@@ -574,6 +587,7 @@ Options:
574
587
 
575
588
  Examples:
576
589
  tender app create --name "Exit Intent Widget" --json
590
+ tender app create --name "Exit Intent Widget" --init --dir ./widget --preview --json
577
591
  tender app create --name "Exit Intent Widget" --profile account --json`;
578
592
  }
579
593
  function artifactsUpdateHelp() {
@@ -590,11 +604,14 @@ Examples:
590
604
  tender app update artifact_123 --name "Sale Widget" --json`;
591
605
  }
592
606
  function artifactsInitHelp() {
593
- return `Usage: tender app init <app-id> [--dir <path>] [--remote <name>] [--base-url <url>] [--token <token>] [--profile <name>] [--ttl <7d|14d|30d>] [--dry-run] [--force] [--json]
607
+ return `Usage: tender app init <app-id> [--dir <path>] [--remote <name>] [--scaffold <server-backed|none>] [--preview] [--base-url <url>] [--token <token>] [--profile <name>] [--ttl <7d|14d|30d>] [--dry-run] [--force] [--json]
594
608
 
595
609
  Options:
596
610
  --dir <path> Local checkout directory to create/update. Defaults to current directory.
597
611
  --remote <name> Git remote name to add or update. Defaults to tender.
612
+ --scaffold <value> API-derived scaffold for empty app source: server-backed or none. Defaults to server-backed.
613
+ --no-scaffold Alias for --scaffold none.
614
+ --preview Request a preview build for the initialized source commit.
598
615
  --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
599
616
  --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
600
617
  --profile <name> Local config profile name. Defaults to the saved default profile.
@@ -606,6 +623,7 @@ Options:
606
623
  Examples:
607
624
  tender app init artifact_123 --dir ./widget --dry-run --json
608
625
  tender app init artifact_123 --dir ./widget --json
626
+ tender app init artifact_123 --dir ./widget --preview --json
609
627
  tender app init artifact_123 --dir ./widget --force --json`;
610
628
  }
611
629
  function artifactsContextFetchHelp() {
@@ -833,6 +851,34 @@ Examples:
833
851
  tender app publish artifact_123 --dry-run --json
834
852
  tender app publish artifact_123 --commit $(git rev-parse HEAD) --confirm publish --stream --json`;
835
853
  }
854
+ function agentHeartbeatHelp() {
855
+ return `Usage: tender app agent heartbeat <app-id> --source <codex|claude-code|tender-cli|git|other> --status <connected|working|waiting_for_user|needs_attention> --summary <text> [options]
856
+
857
+ Options:
858
+ --source <value> Agent identity. Use codex first, claude-code for Claude Code, or other with --source-label.
859
+ --source-label <text> Optional display name when --source other is not specific enough.
860
+ --status <value> connected, working, waiting_for_user, or needs_attention.
861
+ --phase <value> planning, exploring, editing, pushing, validating, building_preview, preview_updating, watching_preview, or waiting.
862
+ --summary <text> Short status line shown in the workspace.
863
+ --detail <text> Optional detail shown in activity history.
864
+ --current-task <text> Optional current task label.
865
+ --commit <sha> Commit SHA related to this update.
866
+ --preview-job <job-id> Preview lifecycle job related to this update.
867
+ --ttl-seconds <seconds> Freshness window. The API clamps this to a bounded range.
868
+ --requested-action <value> none, answer_question, review_preview, or approve_publish. Defaults to none.
869
+ --interruptible Mark the agent as interruptible.
870
+ --input <file|-> Read additional heartbeat JSON fields from a file or stdin. Flags override JSON fields.
871
+ --base-url <url> Tender base URL. Defaults to TENDER_BASE_URL, saved profile base URL, or https://app.tenderprompt.com.
872
+ --token <token> Tender API token. Precedence: --token, explicit --profile, TENDER_API_TOKEN, saved default profile.
873
+ --profile <name> Local config profile name. Defaults to the saved default profile.
874
+ --json Emit stable JSON for coding agents.
875
+
876
+ Examples:
877
+ tender app agent heartbeat artifact_123 --source codex --status connected --summary "Loaded the Tender Prompt skill" --json
878
+ tender app agent heartbeat artifact_123 --source codex --status working --phase exploring --summary "Reading the app structure" --json
879
+ tender app agent heartbeat artifact_123 --source claude-code --status needs_attention --summary "Waiting for an API key decision" --requested-action answer_question --json
880
+ tender app agent heartbeat artifact_123 --input - --json < heartbeat.json`;
881
+ }
836
882
  function publishStatusHelp() {
837
883
  return `Usage: tender app publish status <app-id> --job <job-id> [--base-url <url>] [--token <token>] [--profile <name>] [--json]
838
884
 
@@ -878,6 +924,7 @@ function tenderCliCapabilities() {
878
924
  "tender --help",
879
925
  "tender auth --help",
880
926
  "tender app --help",
927
+ "tender app agent heartbeat --help",
881
928
  "tender app analytics --help",
882
929
  "tender app db --help",
883
930
  "tender app bindings outbound --help",
@@ -892,17 +939,28 @@ function tenderCliCapabilities() {
892
939
  "tender auth login --device --profile edit-preview --artifact <artifact-id> --ttl 7d --json",
893
940
  ],
894
941
  },
942
+ {
943
+ name: "create_new_app_with_preview",
944
+ when: "When the user wants a new Tender App that is immediately visible.",
945
+ commands: [
946
+ "tender app create --name <name> --init --dir <dir> --preview --json",
947
+ ],
948
+ notes: [
949
+ "create --init uses the API-derived scaffold when the new app has no source.",
950
+ "--preview requests a preview build for the initial source commit after the push.",
951
+ ],
952
+ },
895
953
  {
896
954
  name: "clone_or_bootstrap_existing_app",
897
955
  when: "When the user wants to download or work on an existing app locally.",
898
956
  commands: [
899
957
  "tender app init <artifact-id> --dir <dir> --dry-run --json",
900
- "tender app init <artifact-id> --dir <dir> --json",
958
+ "tender app init <artifact-id> --dir <dir> --preview --json",
901
959
  ],
902
960
  notes: [
903
961
  "init is the normal clone/bootstrap command.",
904
962
  "If the app already has artifact Git source, init fetches it and configures the tender remote.",
905
- "If no artifact Git source exists yet, init writes managed context/scaffold files and configures the tender remote.",
963
+ "If no artifact Git source or workspace source exists yet, init fetches the API-derived scaffold, writes managed context files, and configures the tender remote.",
906
964
  "Use context fetch only when you already have a checkout and only need AGENTS.md, skills, or Tender context refreshed.",
907
965
  ],
908
966
  },
@@ -910,14 +968,18 @@ function tenderCliCapabilities() {
910
968
  name: "local_edit_preview_publish",
911
969
  when: "When editing app source from a local checkout.",
912
970
  commands: [
971
+ "tender app agent heartbeat <artifact-id> --source codex --status connected --summary \"Loaded Tender Prompt context\" --json",
913
972
  "tender app doctor --dir <dir> --json",
973
+ "tender app agent heartbeat <artifact-id> --source codex --status working --phase editing --summary \"Editing app files\" --json",
914
974
  "tender app git setup <artifact-id> --dir <dir> --dry-run --json",
915
975
  "git push tender main",
976
+ "tender app agent heartbeat <artifact-id> --source codex --status working --phase building_preview --summary \"Waiting for preview build\" --commit $(git rev-parse HEAD) --json",
916
977
  "tender app preview <artifact-id> --commit $(git rev-parse HEAD) --stream --json",
917
978
  "tender app publish <artifact-id> --dry-run --json",
918
979
  "tender app publish <artifact-id> --commit $(git rev-parse HEAD) --confirm publish --stream --json",
919
980
  ],
920
981
  notes: [
982
+ "Send heartbeat updates only at meaningful milestones or when waiting for user input; do not send rapid polling updates.",
921
983
  "Publishing is production-facing and requires explicit user intent.",
922
984
  ],
923
985
  },
@@ -996,12 +1058,25 @@ function tenderCliCapabilities() {
996
1058
  writes: false,
997
1059
  },
998
1060
  {
999
- command: "tender app init <artifact-id> --dir <dir> --json",
1000
- purpose: "Clone/bootstrap an existing Tender App checkout and configure context plus the tender Git remote.",
1061
+ command: "tender app create --name <name> --init --dir <dir> --preview --json",
1062
+ purpose: "Create a new Tender App, initialize API-derived source in a local checkout, push it, and request a preview build.",
1063
+ requiresAuth: true,
1064
+ writes: true,
1065
+ dryRun: false,
1066
+ },
1067
+ {
1068
+ command: "tender app init <artifact-id> --dir <dir> --preview --json",
1069
+ purpose: "Clone/bootstrap an existing Tender App checkout, configure context plus the tender Git remote, and optionally request a preview for the initialized commit.",
1001
1070
  requiresAuth: true,
1002
1071
  writes: true,
1003
1072
  dryRun: true,
1004
1073
  },
1074
+ {
1075
+ command: "tender app agent heartbeat <artifact-id> --source codex --status working --summary <text> --json",
1076
+ purpose: "Send a concise workspace feedback update from a coding agent.",
1077
+ requiresAuth: true,
1078
+ writes: true,
1079
+ },
1005
1080
  {
1006
1081
  command: "tender app context fetch <artifact-id> --dir <dir> --json",
1007
1082
  purpose: "Fetch managed AGENTS.md, skills, and Tender context into an existing checkout.",
@@ -1285,6 +1360,12 @@ function parseTtl(value) {
1285
1360
  }
1286
1361
  return value;
1287
1362
  }
1363
+ function parseScaffoldTemplate(value) {
1364
+ if (value !== "server-backed" && value !== "none") {
1365
+ throw new TenderCliUsageError("--scaffold must be one of server-backed or none.");
1366
+ }
1367
+ return value;
1368
+ }
1288
1369
  function parseGitRemoteArgs(args) {
1289
1370
  if (args.includes("--help") || args.includes("-h")) {
1290
1371
  return { command: "help", topic: "app git remote" };
@@ -2045,6 +2126,13 @@ function parseArtifactsCreateArgs(args) {
2045
2126
  let name = "";
2046
2127
  let targetPackId = null;
2047
2128
  let targetPackVersion = null;
2129
+ let init = false;
2130
+ let dir = ".";
2131
+ let remoteName = "tender";
2132
+ let ttl = "7d";
2133
+ let force = false;
2134
+ let scaffold = "server-backed";
2135
+ let preview = false;
2048
2136
  let baseUrl = null;
2049
2137
  let flagToken = null;
2050
2138
  let profileName = null;
@@ -2055,6 +2143,22 @@ function parseArtifactsCreateArgs(args) {
2055
2143
  json = true;
2056
2144
  continue;
2057
2145
  }
2146
+ if (arg === "--init") {
2147
+ init = true;
2148
+ continue;
2149
+ }
2150
+ if (arg === "--preview") {
2151
+ preview = true;
2152
+ continue;
2153
+ }
2154
+ if (arg === "--force") {
2155
+ force = true;
2156
+ continue;
2157
+ }
2158
+ if (arg === "--no-scaffold") {
2159
+ scaffold = "none";
2160
+ continue;
2161
+ }
2058
2162
  if (arg === "--name") {
2059
2163
  const value = args[index + 1];
2060
2164
  if (!value || value.startsWith("-")) {
@@ -2082,6 +2186,30 @@ function parseArtifactsCreateArgs(args) {
2082
2186
  index += 1;
2083
2187
  continue;
2084
2188
  }
2189
+ if (arg === "--dir") {
2190
+ const value = args[index + 1];
2191
+ if (!value || value.startsWith("-")) {
2192
+ throw new TenderCliUsageError("--dir requires a path.");
2193
+ }
2194
+ dir = value;
2195
+ index += 1;
2196
+ continue;
2197
+ }
2198
+ if (arg === "--remote") {
2199
+ remoteName = parseRemoteName(args[index + 1]);
2200
+ index += 1;
2201
+ continue;
2202
+ }
2203
+ if (arg === "--ttl") {
2204
+ ttl = parseTtl(args[index + 1]);
2205
+ index += 1;
2206
+ continue;
2207
+ }
2208
+ if (arg === "--scaffold") {
2209
+ scaffold = parseScaffoldTemplate(args[index + 1]);
2210
+ index += 1;
2211
+ continue;
2212
+ }
2085
2213
  if (arg === "--base-url") {
2086
2214
  baseUrl = parseBaseUrlFlag(args, index);
2087
2215
  index += 1;
@@ -2102,11 +2230,38 @@ function parseArtifactsCreateArgs(args) {
2102
2230
  if (!name.trim()) {
2103
2231
  throw new TenderCliUsageError("--name is required. Example: tender app create --name \"Exit Intent Widget\" --json");
2104
2232
  }
2233
+ if (!init) {
2234
+ if (preview) {
2235
+ throw new TenderCliUsageError("--preview requires --init. Example: tender app create --name \"Exit Intent Widget\" --init --dir ./widget --preview --json");
2236
+ }
2237
+ if (force) {
2238
+ throw new TenderCliUsageError("--force is only valid with --init.");
2239
+ }
2240
+ if (dir !== ".") {
2241
+ throw new TenderCliUsageError("--dir is only valid with --init.");
2242
+ }
2243
+ if (remoteName !== "tender") {
2244
+ throw new TenderCliUsageError("--remote is only valid with --init.");
2245
+ }
2246
+ if (ttl !== "7d") {
2247
+ throw new TenderCliUsageError("--ttl is only valid with --init.");
2248
+ }
2249
+ if (scaffold !== "server-backed") {
2250
+ throw new TenderCliUsageError("--scaffold is only valid with --init.");
2251
+ }
2252
+ }
2105
2253
  return {
2106
2254
  command: "app create",
2107
2255
  name: name.trim(),
2108
2256
  targetPackId,
2109
2257
  targetPackVersion,
2258
+ init,
2259
+ dir,
2260
+ remoteName,
2261
+ ttl,
2262
+ force,
2263
+ scaffold,
2264
+ preview,
2110
2265
  baseUrl,
2111
2266
  flagToken,
2112
2267
  profileName,
@@ -2185,6 +2340,8 @@ function parseArtifactsInitArgs(args) {
2185
2340
  let flagToken = null;
2186
2341
  let profileName = null;
2187
2342
  let ttl = "7d";
2343
+ let scaffold = "server-backed";
2344
+ let preview = false;
2188
2345
  let dryRun = false;
2189
2346
  let force = false;
2190
2347
  let json = false;
@@ -2202,6 +2359,14 @@ function parseArtifactsInitArgs(args) {
2202
2359
  force = true;
2203
2360
  continue;
2204
2361
  }
2362
+ if (arg === "--preview") {
2363
+ preview = true;
2364
+ continue;
2365
+ }
2366
+ if (arg === "--no-scaffold") {
2367
+ scaffold = "none";
2368
+ continue;
2369
+ }
2205
2370
  if (arg === "--dir") {
2206
2371
  const value = args[index + 1];
2207
2372
  if (!value || value.startsWith("-")) {
@@ -2236,6 +2401,11 @@ function parseArtifactsInitArgs(args) {
2236
2401
  index += 1;
2237
2402
  continue;
2238
2403
  }
2404
+ if (arg === "--scaffold") {
2405
+ scaffold = parseScaffoldTemplate(args[index + 1]);
2406
+ index += 1;
2407
+ continue;
2408
+ }
2239
2409
  throw new TenderCliUsageError(`Unknown app init option: ${arg}`);
2240
2410
  }
2241
2411
  return {
@@ -2247,6 +2417,8 @@ function parseArtifactsInitArgs(args) {
2247
2417
  flagToken,
2248
2418
  profileName,
2249
2419
  ttl,
2420
+ scaffold,
2421
+ preview,
2250
2422
  dryRun,
2251
2423
  force,
2252
2424
  json,
@@ -3798,6 +3970,204 @@ function parseDbQueryArgs(args) {
3798
3970
  ...parsed,
3799
3971
  };
3800
3972
  }
3973
+ const AGENT_HEARTBEAT_SOURCE_VALUES = new Set([
3974
+ "codex",
3975
+ "claude-code",
3976
+ "tender-cli",
3977
+ "git",
3978
+ "other",
3979
+ ]);
3980
+ const AGENT_HEARTBEAT_STATUS_VALUES = new Set([
3981
+ "connected",
3982
+ "working",
3983
+ "waiting_for_user",
3984
+ "needs_attention",
3985
+ ]);
3986
+ const AGENT_HEARTBEAT_PHASE_VALUES = new Set([
3987
+ "planning",
3988
+ "exploring",
3989
+ "editing",
3990
+ "pushing",
3991
+ "validating",
3992
+ "building_preview",
3993
+ "preview_updating",
3994
+ "watching_preview",
3995
+ "waiting",
3996
+ ]);
3997
+ const AGENT_HEARTBEAT_REQUESTED_ACTION_VALUES = new Set([
3998
+ "answer_question",
3999
+ "review_preview",
4000
+ "approve_publish",
4001
+ "none",
4002
+ ]);
4003
+ function parseAgentHeartbeatEnum(value, flag, allowed) {
4004
+ const parsed = parseRequiredFlagValue(value, flag);
4005
+ if (!allowed.has(parsed)) {
4006
+ throw new TenderCliUsageError(`${flag} must be one of ${Array.from(allowed).join(", ")}.`);
4007
+ }
4008
+ return parsed;
4009
+ }
4010
+ function parseAgentHeartbeatInputPath(value) {
4011
+ if (!value || (value !== "-" && value.startsWith("-"))) {
4012
+ throw new TenderCliUsageError("--input requires a JSON file path or - for stdin.");
4013
+ }
4014
+ return value;
4015
+ }
4016
+ function parseAgentHeartbeatArgs(args) {
4017
+ if (args.includes("--help") || args.includes("-h")) {
4018
+ return { command: "help", topic: "app agent heartbeat" };
4019
+ }
4020
+ const artifactId = args[0];
4021
+ if (!artifactId || artifactId.startsWith("-")) {
4022
+ throw new TenderCliUsageError("app id is required. Example: tender app agent heartbeat <app-id> --source codex --status working --summary \"Reading files\" --json");
4023
+ }
4024
+ let baseUrl = null;
4025
+ let flagToken = null;
4026
+ let profileName = null;
4027
+ let inputPath = null;
4028
+ let source = null;
4029
+ let sourceLabel = null;
4030
+ let status = null;
4031
+ let phase = null;
4032
+ let summary = null;
4033
+ let detail = null;
4034
+ let currentTask = null;
4035
+ let commitSha = null;
4036
+ let previewJobId = null;
4037
+ let ttlSeconds = null;
4038
+ let interruptible = false;
4039
+ let requestedAction = null;
4040
+ let json = false;
4041
+ for (let index = 1; index < args.length; index += 1) {
4042
+ const arg = args[index];
4043
+ const sourceAssignment = parseFlagAssignment(arg, "--source");
4044
+ if (sourceAssignment !== null) {
4045
+ source = parseAgentHeartbeatEnum(sourceAssignment, "--source", AGENT_HEARTBEAT_SOURCE_VALUES);
4046
+ continue;
4047
+ }
4048
+ const statusAssignment = parseFlagAssignment(arg, "--status");
4049
+ if (statusAssignment !== null) {
4050
+ status = parseAgentHeartbeatEnum(statusAssignment, "--status", AGENT_HEARTBEAT_STATUS_VALUES);
4051
+ continue;
4052
+ }
4053
+ const phaseAssignment = parseFlagAssignment(arg, "--phase");
4054
+ if (phaseAssignment !== null) {
4055
+ phase = parseAgentHeartbeatEnum(phaseAssignment, "--phase", AGENT_HEARTBEAT_PHASE_VALUES);
4056
+ continue;
4057
+ }
4058
+ const requestedActionAssignment = parseFlagAssignment(arg, "--requested-action");
4059
+ if (requestedActionAssignment !== null) {
4060
+ requestedAction = parseAgentHeartbeatEnum(requestedActionAssignment, "--requested-action", AGENT_HEARTBEAT_REQUESTED_ACTION_VALUES);
4061
+ continue;
4062
+ }
4063
+ const ttlAssignment = parseFlagAssignment(arg, "--ttl-seconds");
4064
+ if (ttlAssignment !== null) {
4065
+ ttlSeconds = parsePositiveIntegerFlag(ttlAssignment, "--ttl-seconds");
4066
+ continue;
4067
+ }
4068
+ const inputAssignment = parseFlagAssignment(arg, "--input");
4069
+ if (inputAssignment !== null) {
4070
+ inputPath = parseAgentHeartbeatInputPath(inputAssignment);
4071
+ continue;
4072
+ }
4073
+ const textAssignments = [
4074
+ ["--source-label", (value) => { sourceLabel = value; }],
4075
+ ["--summary", (value) => { summary = value; }],
4076
+ ["--detail", (value) => { detail = value; }],
4077
+ ["--current-task", (value) => { currentTask = value; }],
4078
+ ["--commit", (value) => { commitSha = value; }],
4079
+ ["--preview-job", (value) => { previewJobId = value; }],
4080
+ ];
4081
+ const matchedAssignment = textAssignments.find(([flag]) => arg.startsWith(`${flag}=`));
4082
+ if (matchedAssignment) {
4083
+ const [flag, assign] = matchedAssignment;
4084
+ assign(parseRequiredFlagValue(parseFlagAssignment(arg, flag) ?? undefined, flag));
4085
+ continue;
4086
+ }
4087
+ if (arg === "--json") {
4088
+ json = true;
4089
+ continue;
4090
+ }
4091
+ if (arg === "--interruptible") {
4092
+ interruptible = true;
4093
+ continue;
4094
+ }
4095
+ if (arg === "--base-url") {
4096
+ baseUrl = parseBaseUrlFlag(args, index);
4097
+ index += 1;
4098
+ continue;
4099
+ }
4100
+ if (arg === "--token") {
4101
+ flagToken = parseTokenFlag(args, index);
4102
+ index += 1;
4103
+ continue;
4104
+ }
4105
+ if (arg === "--profile") {
4106
+ profileName = parseProfileName(args[index + 1], "--profile");
4107
+ index += 1;
4108
+ continue;
4109
+ }
4110
+ if (arg === "--source") {
4111
+ source = parseAgentHeartbeatEnum(args[index + 1], "--source", AGENT_HEARTBEAT_SOURCE_VALUES);
4112
+ index += 1;
4113
+ continue;
4114
+ }
4115
+ if (arg === "--status") {
4116
+ status = parseAgentHeartbeatEnum(args[index + 1], "--status", AGENT_HEARTBEAT_STATUS_VALUES);
4117
+ index += 1;
4118
+ continue;
4119
+ }
4120
+ if (arg === "--phase") {
4121
+ phase = parseAgentHeartbeatEnum(args[index + 1], "--phase", AGENT_HEARTBEAT_PHASE_VALUES);
4122
+ index += 1;
4123
+ continue;
4124
+ }
4125
+ if (arg === "--requested-action") {
4126
+ requestedAction = parseAgentHeartbeatEnum(args[index + 1], "--requested-action", AGENT_HEARTBEAT_REQUESTED_ACTION_VALUES);
4127
+ index += 1;
4128
+ continue;
4129
+ }
4130
+ if (arg === "--ttl-seconds") {
4131
+ ttlSeconds = parsePositiveIntegerFlag(args[index + 1], "--ttl-seconds");
4132
+ index += 1;
4133
+ continue;
4134
+ }
4135
+ if (arg === "--input") {
4136
+ inputPath = parseAgentHeartbeatInputPath(args[index + 1]);
4137
+ index += 1;
4138
+ continue;
4139
+ }
4140
+ const matchedFlag = textAssignments.find(([flag]) => arg === flag);
4141
+ if (matchedFlag) {
4142
+ const [flag, assign] = matchedFlag;
4143
+ assign(parseRequiredFlagValue(args[index + 1], flag));
4144
+ index += 1;
4145
+ continue;
4146
+ }
4147
+ throw new TenderCliUsageError(`Unknown app agent heartbeat option: ${arg}`);
4148
+ }
4149
+ return {
4150
+ command: "app agent heartbeat",
4151
+ artifactId,
4152
+ baseUrl,
4153
+ flagToken,
4154
+ profileName,
4155
+ inputPath,
4156
+ source,
4157
+ sourceLabel,
4158
+ status,
4159
+ phase,
4160
+ summary,
4161
+ detail,
4162
+ currentTask,
4163
+ commitSha,
4164
+ previewJobId,
4165
+ ttlSeconds,
4166
+ interruptible,
4167
+ requestedAction,
4168
+ json,
4169
+ };
4170
+ }
3801
4171
  function parseTenderArgs(args) {
3802
4172
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
3803
4173
  return { command: "help", topic: "root" };
@@ -3857,6 +4227,18 @@ function parseTenderArgs(args) {
3857
4227
  if (resourceArgs[1] === "context" && resourceArgs[2] === "refresh") {
3858
4228
  return parseContextRefreshArgs(resourceArgs.slice(3));
3859
4229
  }
4230
+ if (resourceArgs[1] === "agent" &&
4231
+ (resourceArgs[2] === undefined ||
4232
+ resourceArgs[2] === "--help" ||
4233
+ resourceArgs[2] === "-h")) {
4234
+ return { command: "help", topic: "app agent heartbeat" };
4235
+ }
4236
+ if (resourceArgs[1] === "agent" && resourceArgs[2] === "heartbeat") {
4237
+ return parseAgentHeartbeatArgs(resourceArgs.slice(3));
4238
+ }
4239
+ if (resourceArgs[1] === "agent") {
4240
+ throw new TenderCliUsageError(`Unknown app agent command: ${resourceArgs[2]}`);
4241
+ }
3860
4242
  if (resourceArgs[1] === "bindings" && resourceArgs[2] === "outbound") {
3861
4243
  return parseBindingsOutboundArgs(resourceArgs.slice(3));
3862
4244
  }
@@ -5246,6 +5628,31 @@ async function requestAgentContext(input) {
5246
5628
  files: payload.files,
5247
5629
  };
5248
5630
  }
5631
+ async function requestArtifactScaffold(input) {
5632
+ const params = new URLSearchParams({ template: input.template });
5633
+ const response = await input.fetcher(`${input.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/scaffold?${params.toString()}`, {
5634
+ headers: {
5635
+ authorization: `Bearer ${input.token}`,
5636
+ },
5637
+ });
5638
+ const payload = await parseJsonResponse(response);
5639
+ if (!response.ok ||
5640
+ !payload?.ok ||
5641
+ !payload.files ||
5642
+ !payload.revision ||
5643
+ payload.template !== input.template) {
5644
+ throw new Error(payload?.message ??
5645
+ payload?.reasonCode ??
5646
+ `Artifact scaffold request failed with HTTP ${response.status}.`);
5647
+ }
5648
+ return {
5649
+ ...payload,
5650
+ artifactId: payload.artifactId ?? input.artifactId,
5651
+ template: payload.template,
5652
+ revision: payload.revision,
5653
+ files: payload.files,
5654
+ };
5655
+ }
5249
5656
  async function runContextFetch(command, io, runtime) {
5250
5657
  const credentials = await resolveApiCredentials({
5251
5658
  baseUrl: command.baseUrl,
@@ -5454,8 +5861,33 @@ async function runArtifactsCreate(command, io, runtime) {
5454
5861
  payload?.reasonCode ??
5455
5862
  `Artifact create request failed with HTTP ${response.status}.`);
5456
5863
  }
5864
+ let initResult = null;
5865
+ let initExitCode = null;
5866
+ if (command.init) {
5867
+ const initStdout = [];
5868
+ initExitCode = await runArtifactsInit({
5869
+ command: "app init",
5870
+ artifactId: payload.artifact.artifactId,
5871
+ dir: command.dir,
5872
+ remoteName: command.remoteName,
5873
+ baseUrl: credentials.baseUrl,
5874
+ flagToken: command.flagToken,
5875
+ profileName: credentials.profileName,
5876
+ ttl: command.ttl,
5877
+ scaffold: command.scaffold,
5878
+ preview: command.preview,
5879
+ dryRun: false,
5880
+ force: command.force,
5881
+ json: true,
5882
+ }, {
5883
+ stdout: (message) => initStdout.push(message),
5884
+ stderr: io.stderr,
5885
+ }, runtime);
5886
+ const initPayload = initStdout.join("\n").trim();
5887
+ initResult = initPayload ? JSON.parse(initPayload) : null;
5888
+ }
5457
5889
  const result = {
5458
- ok: true,
5890
+ ok: initExitCode === null ? true : initExitCode === 0,
5459
5891
  command: command.command,
5460
5892
  baseUrl: credentials.baseUrl,
5461
5893
  authSource: credentials.source,
@@ -5463,7 +5895,9 @@ async function runArtifactsCreate(command, io, runtime) {
5463
5895
  tenantId: payload.tenantId ?? null,
5464
5896
  appId: payload.artifact.artifactId,
5465
5897
  artifact: payload.artifact,
5898
+ init: initResult,
5466
5899
  next: {
5900
+ init: `tender app init ${payload.artifact.artifactId} --dir . --preview`,
5467
5901
  contextFetch: `tender app context fetch ${payload.artifact.artifactId} --dir .`,
5468
5902
  gitSetup: `tender app git setup ${payload.artifact.artifactId} --dir .`,
5469
5903
  },
@@ -5476,10 +5910,13 @@ async function runArtifactsCreate(command, io, runtime) {
5476
5910
  `app_id: ${payload.artifact.artifactId}`,
5477
5911
  `name: ${payload.artifact.name}`,
5478
5912
  `preview_url: ${payload.artifact.previewUrl ?? ""}`,
5479
- `next: ${result.next.contextFetch}`,
5480
- ].join("\n"));
5913
+ initExitCode === null ? `next: ${result.next.init}` : null,
5914
+ initExitCode !== null ? `init_exit_code: ${initExitCode}` : null,
5915
+ ]
5916
+ .filter((line) => Boolean(line))
5917
+ .join("\n"));
5481
5918
  }
5482
- return 0;
5919
+ return initExitCode ?? 0;
5483
5920
  }
5484
5921
  async function runArtifactsUpdate(command, io, runtime) {
5485
5922
  const credentials = await resolveApiCredentials({
@@ -5563,7 +6000,7 @@ function shellQuote(value) {
5563
6000
  function createCredentialHelperCommand(input) {
5564
6001
  const parts = [
5565
6002
  "tender",
5566
- "artifacts",
6003
+ "app",
5567
6004
  "git",
5568
6005
  "credential",
5569
6006
  shellQuote(input.artifactId),
@@ -5736,6 +6173,7 @@ async function maybeSeedInitialArtifactGitCommit(input) {
5736
6173
  skippedReason: "local_head_exists",
5737
6174
  commitExitCode: null,
5738
6175
  pushExitCode: null,
6176
+ commitSha: null,
5739
6177
  };
5740
6178
  }
5741
6179
  if (input.sourceCheckout.checkedOut) {
@@ -5746,6 +6184,7 @@ async function maybeSeedInitialArtifactGitCommit(input) {
5746
6184
  skippedReason: "source_checked_out",
5747
6185
  commitExitCode: null,
5748
6186
  pushExitCode: null,
6187
+ commitSha: null,
5749
6188
  };
5750
6189
  }
5751
6190
  if (input.sourceCheckout.skippedReason !== "remote_default_branch_missing") {
@@ -5756,6 +6195,7 @@ async function maybeSeedInitialArtifactGitCommit(input) {
5756
6195
  skippedReason: "remote_unavailable",
5757
6196
  commitExitCode: null,
5758
6197
  pushExitCode: null,
6198
+ commitSha: null,
5759
6199
  };
5760
6200
  }
5761
6201
  const paths = [...new Set(input.paths)].sort();
@@ -5767,6 +6207,7 @@ async function maybeSeedInitialArtifactGitCommit(input) {
5767
6207
  skippedReason: "no_files_to_seed",
5768
6208
  commitExitCode: null,
5769
6209
  pushExitCode: null,
6210
+ commitSha: null,
5770
6211
  };
5771
6212
  }
5772
6213
  await runGitCommand({
@@ -5779,6 +6220,11 @@ async function maybeSeedInitialArtifactGitCommit(input) {
5779
6220
  cwd: input.root,
5780
6221
  args: ["commit", "-m", "Seed Tender App scaffold"],
5781
6222
  });
6223
+ const commitShaResult = await runGitCommand({
6224
+ runtime: input.runtime,
6225
+ cwd: input.root,
6226
+ args: ["rev-parse", "HEAD"],
6227
+ });
5782
6228
  const pushResult = await runGitCommand({
5783
6229
  runtime: input.runtime,
5784
6230
  cwd: input.root,
@@ -5791,6 +6237,7 @@ async function maybeSeedInitialArtifactGitCommit(input) {
5791
6237
  skippedReason: null,
5792
6238
  commitExitCode: commitResult.exitCode,
5793
6239
  pushExitCode: pushResult.exitCode,
6240
+ commitSha: commitShaResult.stdout.trim() || null,
5794
6241
  };
5795
6242
  }
5796
6243
  async function requestGitRemoteLease(input) {
@@ -5808,6 +6255,26 @@ async function requestGitRemoteLease(input) {
5808
6255
  }
5809
6256
  return payload.git;
5810
6257
  }
6258
+ async function requestPreviewRebuildJob(input) {
6259
+ const response = await input.fetcher(`${input.credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(input.artifactId)}/preview/rebuild`, {
6260
+ method: "POST",
6261
+ headers: {
6262
+ authorization: `Bearer ${input.credentials.token}`,
6263
+ "content-type": "application/json",
6264
+ },
6265
+ body: JSON.stringify({ commitSha: input.commitSha }),
6266
+ });
6267
+ const payload = await parseJsonResponse(response);
6268
+ if (!response.ok || !payload?.ok) {
6269
+ throw new Error(payload?.message ??
6270
+ payload?.reasonCode ??
6271
+ `Artifact preview request failed with HTTP ${response.status}.`);
6272
+ }
6273
+ return {
6274
+ job: payload.job ?? null,
6275
+ result: payload.result ?? null,
6276
+ };
6277
+ }
5811
6278
  function parseCredentialInput(stdin) {
5812
6279
  const values = new Map();
5813
6280
  for (const line of stdin.split(/\r?\n/)) {
@@ -6010,13 +6477,28 @@ async function runArtifactsInit(command, io, runtime) {
6010
6477
  skippedReason: command.dryRun ? "dry_run" : null,
6011
6478
  commitExitCode: null,
6012
6479
  pushExitCode: null,
6480
+ commitSha: null,
6013
6481
  };
6014
6482
  let sourceExport = {
6015
6483
  attempted: false,
6484
+ source: null,
6485
+ template: null,
6486
+ revision: null,
6016
6487
  filesWritten: 0,
6017
6488
  skippedReason: command.dryRun ? "dry_run" : null,
6018
6489
  files: [],
6019
6490
  };
6491
+ let preview = {
6492
+ attempted: false,
6493
+ commitSha: null,
6494
+ skippedReason: command.preview
6495
+ ? command.dryRun
6496
+ ? "dry_run"
6497
+ : null
6498
+ : "not_requested",
6499
+ job: null,
6500
+ result: null,
6501
+ };
6020
6502
  let seedPaths = [];
6021
6503
  let plannedFiles = await planContextFiles({
6022
6504
  root,
@@ -6052,12 +6534,31 @@ async function runArtifactsInit(command, io, runtime) {
6052
6534
  defaultBranch: git.defaultBranch,
6053
6535
  });
6054
6536
  if (sourceCheckout.skippedReason === "remote_default_branch_missing") {
6055
- const sourceFiles = await collectArtifactWorkspaceSourceFiles({
6537
+ let sourceFiles = await collectArtifactWorkspaceSourceFiles({
6056
6538
  artifactId: command.artifactId,
6057
6539
  baseUrl: credentials.baseUrl,
6058
6540
  token: credentials.token,
6059
6541
  fetcher,
6060
6542
  });
6543
+ let source = sourceFiles.length > 0 ? "workspace" : null;
6544
+ let scaffoldRevision = null;
6545
+ let scaffoldTemplate = null;
6546
+ if (sourceFiles.length === 0 && command.scaffold !== "none") {
6547
+ const scaffold = await requestArtifactScaffold({
6548
+ artifactId: command.artifactId,
6549
+ template: command.scaffold,
6550
+ baseUrl: credentials.baseUrl,
6551
+ token: credentials.token,
6552
+ fetcher,
6553
+ });
6554
+ sourceFiles = scaffold.files.map((file) => ({
6555
+ path: file.path,
6556
+ content: file.content,
6557
+ }));
6558
+ source = "api_scaffold";
6559
+ scaffoldRevision = scaffold.revision;
6560
+ scaffoldTemplate = scaffold.template;
6561
+ }
6061
6562
  const plannedSourceFiles = await planSourceFiles({
6062
6563
  root,
6063
6564
  files: sourceFiles,
@@ -6066,8 +6567,15 @@ async function runArtifactsInit(command, io, runtime) {
6066
6567
  const sourceConflicts = plannedSourceFiles.filter((file) => file.action === "conflict");
6067
6568
  sourceExport = {
6068
6569
  attempted: true,
6570
+ source,
6571
+ template: scaffoldTemplate,
6572
+ revision: scaffoldRevision,
6069
6573
  filesWritten: 0,
6070
- skippedReason: plannedSourceFiles.length === 0 ? "empty_workspace" : null,
6574
+ skippedReason: plannedSourceFiles.length === 0
6575
+ ? command.scaffold === "none"
6576
+ ? "scaffold_disabled"
6577
+ : "empty_workspace"
6578
+ : null,
6071
6579
  files: plannedSourceFiles.map((file) => ({
6072
6580
  path: file.path,
6073
6581
  action: file.action,
@@ -6133,6 +6641,9 @@ async function runArtifactsInit(command, io, runtime) {
6133
6641
  else if (sourceCheckout.checkedOut) {
6134
6642
  sourceExport = {
6135
6643
  attempted: false,
6644
+ source: null,
6645
+ template: null,
6646
+ revision: null,
6136
6647
  filesWritten: 0,
6137
6648
  skippedReason: "source_checked_out",
6138
6649
  files: [],
@@ -6141,6 +6652,9 @@ async function runArtifactsInit(command, io, runtime) {
6141
6652
  else if (sourceCheckout.skippedReason === "local_head_exists") {
6142
6653
  sourceExport = {
6143
6654
  attempted: false,
6655
+ source: null,
6656
+ template: null,
6657
+ revision: null,
6144
6658
  filesWritten: 0,
6145
6659
  skippedReason: "local_head_exists",
6146
6660
  files: [],
@@ -6149,6 +6663,9 @@ async function runArtifactsInit(command, io, runtime) {
6149
6663
  else if (sourceCheckout.skippedReason === "remote_unavailable") {
6150
6664
  sourceExport = {
6151
6665
  attempted: false,
6666
+ source: null,
6667
+ template: null,
6668
+ revision: null,
6152
6669
  filesWritten: 0,
6153
6670
  skippedReason: "remote_unavailable",
6154
6671
  files: [],
@@ -6220,6 +6737,45 @@ async function runArtifactsInit(command, io, runtime) {
6220
6737
  sourceCheckout,
6221
6738
  paths: seedPaths,
6222
6739
  });
6740
+ if (command.preview) {
6741
+ let commitSha = seedCommit.commitSha;
6742
+ if (!commitSha && (seedCommit.pushed || sourceCheckout.checkedOut)) {
6743
+ const headResult = await runGitCommand({
6744
+ runtime,
6745
+ cwd: root,
6746
+ args: ["rev-parse", "HEAD"],
6747
+ allowFailure: true,
6748
+ });
6749
+ commitSha =
6750
+ headResult.exitCode === 0 ? headResult.stdout.trim() || null : null;
6751
+ }
6752
+ if (!commitSha) {
6753
+ preview = {
6754
+ attempted: false,
6755
+ commitSha: null,
6756
+ skippedReason: seedCommit.pushed || sourceCheckout.checkedOut
6757
+ ? "commit_unavailable"
6758
+ : "seed_not_pushed",
6759
+ job: null,
6760
+ result: null,
6761
+ };
6762
+ }
6763
+ else {
6764
+ const previewJob = await requestPreviewRebuildJob({
6765
+ artifactId: command.artifactId,
6766
+ commitSha,
6767
+ credentials,
6768
+ fetcher,
6769
+ });
6770
+ preview = {
6771
+ attempted: true,
6772
+ commitSha,
6773
+ skippedReason: null,
6774
+ job: previewJob.job,
6775
+ result: previewJob.result,
6776
+ };
6777
+ }
6778
+ }
6223
6779
  }
6224
6780
  const result = {
6225
6781
  ok: true,
@@ -6233,11 +6789,13 @@ async function runArtifactsInit(command, io, runtime) {
6233
6789
  defaultBranch: git.defaultBranch,
6234
6790
  dryRun: command.dryRun,
6235
6791
  force: command.force,
6792
+ scaffold: command.scaffold,
6236
6793
  authSource: credentials.source,
6237
6794
  profile: credentials.profileName,
6238
6795
  sourceCheckout,
6239
6796
  sourceExport,
6240
6797
  seedCommit,
6798
+ preview,
6241
6799
  summary: {
6242
6800
  create: plannedFiles.filter((file) => file.action === "create").length,
6243
6801
  overwrite: plannedFiles.filter((file) => file.action === "overwrite").length,
@@ -6267,8 +6825,13 @@ async function runArtifactsInit(command, io, runtime) {
6267
6825
  `context_revision: ${context.revision}`,
6268
6826
  `dir: ${root}`,
6269
6827
  `remote: ${command.remoteName}`,
6828
+ preview.attempted && preview.job?.jobId
6829
+ ? `preview_job: ${preview.job.jobId}`
6830
+ : null,
6270
6831
  `next: ${result.next}`,
6271
- ].join("\n"));
6832
+ ]
6833
+ .filter((line) => Boolean(line))
6834
+ .join("\n"));
6272
6835
  }
6273
6836
  return 0;
6274
6837
  }
@@ -6666,6 +7229,109 @@ async function runPublishWatch(command, io, runtime) {
6666
7229
  }
6667
7230
  return 0;
6668
7231
  }
7232
+ async function readAgentHeartbeatInputObject(input) {
7233
+ if (!input.command.inputPath) {
7234
+ return {};
7235
+ }
7236
+ const raw = input.command.inputPath === "-"
7237
+ ? input.stdin
7238
+ : await readFile(path.resolve(input.command.inputPath), "utf8");
7239
+ if (!raw) {
7240
+ throw new TenderCliUsageError("--input - requires heartbeat JSON on stdin.");
7241
+ }
7242
+ let parsed;
7243
+ try {
7244
+ parsed = JSON.parse(raw);
7245
+ }
7246
+ catch {
7247
+ throw new TenderCliUsageError("--input must contain valid JSON.");
7248
+ }
7249
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
7250
+ throw new TenderCliUsageError("--input must contain a JSON object.");
7251
+ }
7252
+ return parsed;
7253
+ }
7254
+ function assignAgentHeartbeatField(body, field, value) {
7255
+ if (value !== null) {
7256
+ body[field] = value;
7257
+ }
7258
+ }
7259
+ async function buildAgentHeartbeatBody(command, runtime) {
7260
+ const body = await readAgentHeartbeatInputObject({
7261
+ command,
7262
+ stdin: runtime.stdin,
7263
+ });
7264
+ assignAgentHeartbeatField(body, "source", command.source);
7265
+ assignAgentHeartbeatField(body, "sourceLabel", command.sourceLabel);
7266
+ assignAgentHeartbeatField(body, "status", command.status);
7267
+ assignAgentHeartbeatField(body, "phase", command.phase);
7268
+ assignAgentHeartbeatField(body, "summary", command.summary);
7269
+ assignAgentHeartbeatField(body, "detail", command.detail);
7270
+ assignAgentHeartbeatField(body, "currentTask", command.currentTask);
7271
+ assignAgentHeartbeatField(body, "commitSha", command.commitSha);
7272
+ assignAgentHeartbeatField(body, "previewJobId", command.previewJobId);
7273
+ assignAgentHeartbeatField(body, "ttlSeconds", command.ttlSeconds);
7274
+ assignAgentHeartbeatField(body, "requestedAction", command.requestedAction);
7275
+ if (command.interruptible) {
7276
+ body.interruptible = true;
7277
+ }
7278
+ if (typeof body.source !== "string" || body.source.trim().length === 0) {
7279
+ throw new TenderCliUsageError("--source is required. Example: tender app agent heartbeat <app-id> --source codex --status working --summary \"Reading files\" --json");
7280
+ }
7281
+ if (typeof body.status !== "string" || body.status.trim().length === 0) {
7282
+ throw new TenderCliUsageError("--status is required. Example: tender app agent heartbeat <app-id> --source codex --status working --summary \"Reading files\" --json");
7283
+ }
7284
+ if (typeof body.summary !== "string" || body.summary.trim().length === 0) {
7285
+ throw new TenderCliUsageError("--summary is required. Example: tender app agent heartbeat <app-id> --source codex --status working --summary \"Reading files\" --json");
7286
+ }
7287
+ return body;
7288
+ }
7289
+ async function runAgentHeartbeat(command, io, runtime) {
7290
+ const credentials = await resolveApiCredentials({
7291
+ baseUrl: command.baseUrl,
7292
+ flagToken: command.flagToken,
7293
+ profileName: command.profileName,
7294
+ env: runtime.env ?? process.env,
7295
+ });
7296
+ const body = await buildAgentHeartbeatBody(command, runtime);
7297
+ const response = await (runtime.fetcher ?? fetch)(`${credentials.baseUrl}/api/v1/artifacts/${encodeURIComponent(command.artifactId)}/agent/heartbeat`, {
7298
+ method: "POST",
7299
+ headers: {
7300
+ authorization: `Bearer ${credentials.token}`,
7301
+ accept: "application/json",
7302
+ "content-type": "application/json",
7303
+ },
7304
+ body: JSON.stringify(body),
7305
+ });
7306
+ const payload = await parseJsonResponse(response);
7307
+ if (!response.ok || !payload?.ok) {
7308
+ throw new Error((typeof payload?.message === "string" && payload.message) ||
7309
+ (typeof payload?.reasonCode === "string" && payload.reasonCode) ||
7310
+ `Agent heartbeat failed with HTTP ${response.status}.`);
7311
+ }
7312
+ const result = {
7313
+ ok: true,
7314
+ command: command.command,
7315
+ baseUrl: credentials.baseUrl,
7316
+ authSource: credentials.source,
7317
+ profile: credentials.profileName,
7318
+ tenantId: payload.tenantId ?? null,
7319
+ artifactId: payload.artifactId ?? command.artifactId,
7320
+ activity: payload.activity ?? null,
7321
+ };
7322
+ if (command.json) {
7323
+ io.stdout(JSON.stringify(result, null, 2));
7324
+ }
7325
+ else {
7326
+ io.stdout([
7327
+ `app_id: ${result.artifactId}`,
7328
+ `heartbeat: accepted`,
7329
+ `status: ${String(body.status)}`,
7330
+ `summary: ${String(body.summary)}`,
7331
+ ].join("\n"));
7332
+ }
7333
+ return 0;
7334
+ }
6669
7335
  async function runGitRemote(command, io, runtime) {
6670
7336
  const credentials = await resolveApiCredentials({
6671
7337
  baseUrl: command.baseUrl,
@@ -7433,6 +8099,9 @@ export async function runTenderCli(args, io = {
7433
8099
  else if (command.topic === "app preview watch") {
7434
8100
  io.stdout(previewWatchHelp());
7435
8101
  }
8102
+ else if (command.topic === "app agent heartbeat") {
8103
+ io.stdout(agentHeartbeatHelp());
8104
+ }
7436
8105
  else if (command.topic === "app publish") {
7437
8106
  io.stdout(publishHelp());
7438
8107
  }
@@ -7573,6 +8242,9 @@ export async function runTenderCli(args, io = {
7573
8242
  if (command.command === "app preview watch") {
7574
8243
  return await runPreviewWatch(command, io, runtime);
7575
8244
  }
8245
+ if (command.command === "app agent heartbeat") {
8246
+ return await runAgentHeartbeat(command, io, runtime);
8247
+ }
7576
8248
  if (command.command === "app publish") {
7577
8249
  return await runPublish(command, io, runtime);
7578
8250
  }
@@ -7639,10 +8311,15 @@ export async function runTenderCli(args, io = {
7639
8311
  if (command.command === "app git remote") {
7640
8312
  return await runGitRemote(command, io, runtime);
7641
8313
  }
7642
- return await runDoctor(command, io);
8314
+ if (command.command === "app doctor") {
8315
+ return await runDoctor(command, io);
8316
+ }
8317
+ throw new TenderCliUsageError("Unhandled command.");
7643
8318
  }
7644
8319
  catch (error) {
7645
- if (args.includes("--json") && args[0] === "app" && args[1] === "db") {
8320
+ if (args.includes("--json") &&
8321
+ args[0] === "app" &&
8322
+ (args[1] === "db" || (args[1] === "agent" && args[2] === "heartbeat"))) {
7646
8323
  io.stdout(JSON.stringify(cliErrorPayload(error, args, command), null, 2));
7647
8324
  return error instanceof TenderCliUsageError ? 2 : 1;
7648
8325
  }
@@ -7659,6 +8336,9 @@ export function shouldReadEntrypointStdin(args) {
7659
8336
  return (args[0] === "app" &&
7660
8337
  ((args[1] === "git" && args[2] === "credential") ||
7661
8338
  (args[1] === "analytics" && hasFlagValue(args, "--spec", "-")) ||
8339
+ (args[1] === "agent" &&
8340
+ args[2] === "heartbeat" &&
8341
+ hasFlagValue(args, "--input", "-")) ||
7662
8342
  (args[1] === "db" &&
7663
8343
  args[2] === "query" &&
7664
8344
  hasFlagValue(args, "--file", "-"))));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tenderprompt/cli",
3
- "version": "0.1.15",
3
+ "version": "0.1.16",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "tender": "bin/tender.js"