@nanhara/hara 0.127.2 → 0.129.0

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.
@@ -37,12 +37,28 @@ import { readModelContextFileSync } from "../fs-read.js";
37
37
  import { optionalPosixOpenFlag } from "../fs-open-flags.js";
38
38
  import { sameOpenedFileIdentity } from "../fs-identity.js";
39
39
  import { redactSensitiveText, redactSensitiveValue } from "../security/secrets.js";
40
+ import { ArtifactStoreError, commitArtifact, getArtifact, importArtifact, listArtifactRevisions, listArtifacts, revertArtifact, } from "../artifacts/store.js";
40
41
  import { consumePendingTaskSteering, createTaskExecution, continueTaskExecution, finishTaskExecution, newSteerInteraction, newTurnInteraction, recordTaskSteering, requestsTaskContinuation, taskExecutionContext, } from "../session/task.js";
41
42
  const APPROVAL_TIMEOUT_MS = 300_000; // an unanswered approval denies after 5 min (never hangs a turn)
42
43
  const COMPACT_TIMEOUT_MS = 60_000;
43
44
  const SHUTDOWN_GRACE_MS = 2_000;
44
45
  const SOCKET_CLOSE_GRACE_MS = 250;
45
46
  const DISCOVERY_LOCK_WAIT_MS = 2_000;
47
+ const artifactRpcError = (id, error, action) => {
48
+ if (error instanceof ArtifactStoreError) {
49
+ const code = error.code === "ARTIFACT_CORRUPT"
50
+ ? ERR.INTERNAL
51
+ : error.code === "ARTIFACT_CONFLICT"
52
+ ? ERR.CONFLICT
53
+ : ERR.PARAMS;
54
+ return rpcError(id, code, error.message);
55
+ }
56
+ return rpcError(id, ERR.INTERNAL, action === "import" || action === "commit"
57
+ ? `Artifact ${action} failed safely; the source file was not modified`
58
+ : action === "revert"
59
+ ? "Artifact revert failed safely; no current revision was replaced"
60
+ : `Artifact ${action} failed safely; local Artifact data was not changed`);
61
+ };
46
62
  const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
47
63
  const isPidAlive = (pid) => {
48
64
  if (!Number.isSafeInteger(pid) || pid <= 0)
@@ -349,6 +365,7 @@ export async function startServe(opts, deps) {
349
365
  };
350
366
  // Discovery file — the desktop shell reads this to find the running server (like a pid/port file).
351
367
  const discoveryDir = join(deps.discoveryHome ?? homedir(), ".hara");
368
+ const artifactHome = deps.artifactHome ?? homedir();
352
369
  const discoveryPath = join(discoveryDir, "serve.json");
353
370
  const discovery = { host: opts.host, port, token, pid: process.pid, version: deps.version, instanceId };
354
371
  if (!deps.quietDiscovery) {
@@ -731,6 +748,8 @@ export async function startServe(opts, deps) {
731
748
  "approval.reply", "plugins.list", "plugins.set", "skills.list", "models.list", "files.search", "project.panels",
732
749
  "settings.providers.list", "settings.providers.test", "settings.providers.save",
733
750
  "automation.list", "automation.add", "automation.toggle", "automation.delete",
751
+ "artifact.import", "artifact.commit", "artifact.revert",
752
+ "artifact.list", "artifact.get", "artifact.revisions",
734
753
  "tasks.list", "approvals.list", "approvals.resolve",
735
754
  ];
736
755
  const runtime = runtimeInfo();
@@ -1126,6 +1145,118 @@ export async function startServe(opts, deps) {
1126
1145
  return reply(rpcError(id, ERR.PARAMS, `no job ${p.id}`));
1127
1146
  return reply(rpcResult(id, { id: p.id, deleted: true }));
1128
1147
  }
1148
+ case "artifact.import": {
1149
+ if (typeof p.sourcePath !== "string" || !p.sourcePath) {
1150
+ return reply(rpcError(id, ERR.PARAMS, "sourcePath required"));
1151
+ }
1152
+ if (p.kind !== undefined
1153
+ && p.kind !== "presentation"
1154
+ && p.kind !== "spreadsheet"
1155
+ && p.kind !== "document")
1156
+ return reply(rpcError(id, ERR.PARAMS, "kind must be presentation, spreadsheet, or document"));
1157
+ if (p.title !== undefined && typeof p.title !== "string") {
1158
+ return reply(rpcError(id, ERR.PARAMS, "title must be a string"));
1159
+ }
1160
+ try {
1161
+ const details = await importArtifact(artifactHome, {
1162
+ sourcePath: p.sourcePath,
1163
+ ...(p.title !== undefined ? { title: p.title } : {}),
1164
+ ...(p.kind !== undefined ? { kind: p.kind } : {}),
1165
+ });
1166
+ return reply(rpcResult(id, details));
1167
+ }
1168
+ catch (error) {
1169
+ return reply(artifactRpcError(id, error, "import"));
1170
+ }
1171
+ }
1172
+ case "artifact.commit": {
1173
+ if (typeof p.artifactId !== "string"
1174
+ || typeof p.baseRevisionId !== "string"
1175
+ || typeof p.sourcePath !== "string"
1176
+ || !p.sourcePath) {
1177
+ return reply(rpcError(id, ERR.PARAMS, "artifactId, baseRevisionId, and sourcePath required"));
1178
+ }
1179
+ if (p.actor !== undefined) {
1180
+ return reply(rpcError(id, ERR.PARAMS, "actor is assigned by the authenticated host"));
1181
+ }
1182
+ if (p.taskRunId !== undefined && typeof p.taskRunId !== "string") {
1183
+ return reply(rpcError(id, ERR.PARAMS, "taskRunId must be a string"));
1184
+ }
1185
+ if (p.changedPaths !== undefined && !Array.isArray(p.changedPaths)) {
1186
+ return reply(rpcError(id, ERR.PARAMS, "changedPaths must be an array"));
1187
+ }
1188
+ try {
1189
+ const details = await commitArtifact(artifactHome, {
1190
+ artifactId: p.artifactId,
1191
+ baseRevisionId: p.baseRevisionId,
1192
+ sourcePath: p.sourcePath,
1193
+ actor: "user",
1194
+ ...(p.taskRunId !== undefined ? { taskRunId: p.taskRunId } : {}),
1195
+ ...(p.changedPaths !== undefined ? { changedPaths: p.changedPaths } : {}),
1196
+ });
1197
+ return reply(rpcResult(id, details));
1198
+ }
1199
+ catch (error) {
1200
+ return reply(artifactRpcError(id, error, "commit"));
1201
+ }
1202
+ }
1203
+ case "artifact.revert": {
1204
+ if (typeof p.artifactId !== "string"
1205
+ || typeof p.baseRevisionId !== "string"
1206
+ || typeof p.targetRevisionId !== "string") {
1207
+ return reply(rpcError(id, ERR.PARAMS, "artifactId, baseRevisionId, and targetRevisionId required"));
1208
+ }
1209
+ if (p.actor !== undefined) {
1210
+ return reply(rpcError(id, ERR.PARAMS, "actor is assigned by the authenticated host"));
1211
+ }
1212
+ if (p.taskRunId !== undefined && typeof p.taskRunId !== "string") {
1213
+ return reply(rpcError(id, ERR.PARAMS, "taskRunId must be a string"));
1214
+ }
1215
+ try {
1216
+ const details = revertArtifact(artifactHome, {
1217
+ artifactId: p.artifactId,
1218
+ baseRevisionId: p.baseRevisionId,
1219
+ targetRevisionId: p.targetRevisionId,
1220
+ actor: "user",
1221
+ ...(p.taskRunId !== undefined ? { taskRunId: p.taskRunId } : {}),
1222
+ });
1223
+ return reply(rpcResult(id, details));
1224
+ }
1225
+ catch (error) {
1226
+ return reply(artifactRpcError(id, error, "revert"));
1227
+ }
1228
+ }
1229
+ case "artifact.list": {
1230
+ try {
1231
+ return reply(rpcResult(id, listArtifacts(artifactHome)));
1232
+ }
1233
+ catch (error) {
1234
+ return reply(artifactRpcError(id, error, "list"));
1235
+ }
1236
+ }
1237
+ case "artifact.get": {
1238
+ if (typeof p.artifactId !== "string") {
1239
+ return reply(rpcError(id, ERR.PARAMS, "artifactId required"));
1240
+ }
1241
+ try {
1242
+ return reply(rpcResult(id, getArtifact(artifactHome, p.artifactId)));
1243
+ }
1244
+ catch (error) {
1245
+ return reply(artifactRpcError(id, error, "open"));
1246
+ }
1247
+ }
1248
+ case "artifact.revisions": {
1249
+ if (typeof p.artifactId !== "string") {
1250
+ return reply(rpcError(id, ERR.PARAMS, "artifactId required"));
1251
+ }
1252
+ try {
1253
+ const revisions = listArtifactRevisions(artifactHome, p.artifactId);
1254
+ return reply(rpcResult(id, { artifactId: p.artifactId, revisions }));
1255
+ }
1256
+ catch (error) {
1257
+ return reply(artifactRpcError(id, error, "list revisions"));
1258
+ }
1259
+ }
1129
1260
  case "skills.list": {
1130
1261
  const cwd = typeof p.cwd === "string" && p.cwd ? p.cwd : opts.cwd;
1131
1262
  return reply(rpcResult(id, { skills: loadSkillIndex(cwd).map((s) => ({ id: s.id, description: s.description, source: s.source })) }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanhara/hara",
3
- "version": "0.127.2",
3
+ "version": "0.129.0",
4
4
  "description": "hara — a coding agent CLI that runs like an engineering org.",
5
5
  "bin": {
6
6
  "hara": "runtime-bootstrap.cjs"