@@ -25,9 +25,12 @@ sys.path.insert(0, os.path.dirname(os.path.realpath(__file__)))
25import prompts # noqa: E402
25import prompts # noqa: E402
26import validate # noqa: E402
26import validate # noqa: E402
28−HOST_VERSION = "0.1.0"
28+HOST_VERSION = "0.1.0" # release builds stamp the extension version here (scripts/build.mjs)
29HOST_NAME = "io.diagonal.host"
29HOST_NAME = "io.diagonal.host"
30ALLOWED_ORIGIN = "chrome-extension://mpnodlalikgeehnlnofdkpgapkmbkjdf/"
30ALLOWED_ORIGIN = "chrome-extension://mpnodlalikgeehnlnofdkpgapkmbkjdf/"
31+# The Chrome Web Store gives the listed extension its own ID. Once the listing exists, put its origin here
32+# ("chrome-extension://<store id>/") so the store install and the unpacked one can both use the host.
31FM = os.environ.get("DIAGONAL_FM", "/usr/bin/fm") # tests point this at host/tests/fake_fm.py
34FM = os.environ.get("DIAGONAL_FM", "/usr/bin/fm") # tests point this at host/tests/fake_fm.py
32SUPPORT = os.environ.get("DIAGONAL_SUPPORT_DIR", os.path.expanduser("~/Library/Application Support/Diagonal"))
35SUPPORT = os.environ.get("DIAGONAL_SUPPORT_DIR", os.path.expanduser("~/Library/Application Support/Diagonal"))
33SCHEMAS = os.path.join(SUPPORT, "schemas")
36SCHEMAS = os.path.join(SUPPORT, "schemas")
@@ -247,11 +250,18 @@ def op_ping(_payload, _opts):
247 license_required, msg = True, LICENSE_MESSAGE
250 license_required, msg = True, LICENSE_MESSAGE
248 except subprocess.TimeoutExpired:
251 except subprocess.TimeoutExpired:
249 ok, msg = False, "fm available timed out"
252 ok, msg = False, "fm available timed out"
253+ schema_message = None
251− ensure_schemas()
256+ ensure_schemas()
258+ schema_message = f.message
252 mode = organize_mode()
259 mode = organize_mode()
253− return {"hostVersion": HOST_VERSION, "fmPath": FM, "fmAvailable": ok, "fmMessage": msg,
260+ reply = {"hostVersion": HOST_VERSION, "fmPath": FM, "fmAvailable": ok, "fmMessage": msg,
254− "licenseRequired": license_required, "schemasOk": schemas_ok(), "organizeMode": mode}
261+ "licenseRequired": license_required, "schemasOk": schemas_ok(), "organizeMode": mode}
263+ reply["schemaMessage"] = schema_message
257def schemas_ok():
267def schemas_ok():
@@ -263,17 +273,22 @@ def ensure_schemas():
263 accepted after the installer ran). stdout is the native-messaging channel: progress lines are swallowed."""
273 accepted after the installer ran). stdout is the native-messaging channel: progress lines are swallowed."""
264 if schemas_ok():
274 if schemas_ok():
266− with contextlib.redirect_stdout(io.StringIO()), contextlib.redirect_stderr(io.StringIO()):
277+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(out):
267 rc = install_schemas()
278 rc = install_schemas()
268 if rc == INSTALL_LICENSE_EXIT:
279 if rc == INSTALL_LICENSE_EXIT:
269 raise Fail("LICENSE_REQUIRED", LICENSE_MESSAGE)
280 raise Fail("LICENSE_REQUIRED", LICENSE_MESSAGE)
281+ if not schemas_ok():
282+ lines = [ln for ln in out.getvalue().splitlines() if ln.strip()]
283+ why = lines[-1] if lines else f"fm schema exited {rc}"
284+ raise Fail("SCHEMA_MISSING", f"fm could not write Diagonal's schema files: {why}"[:500], raw=out.getvalue()[-2000:])
272def op_name(payload, opts):
287def op_name(payload, opts):
273 ensure_schemas()
288 ensure_schemas()
274 schema = schema_path("name.json")
289 schema = schema_path("name.json")
275 if not schema:
290 if not schema:
276− raise Fail("SCHEMA_MISSING", os.path.join(SCHEMAS, "name.json"))
291+ raise Fail("SCHEMA_MISSING", f"fm could not write {os.path.join(SCHEMAS, 'name.json')}")
277 prompt = prompts.build_name_prompt(payload, strict=opts.get("strict", False))
292 prompt = prompts.build_name_prompt(payload, strict=opts.get("strict", False))
278 check_budget(prompt, len(payload["items"]))
293 check_budget(prompt, len(payload["items"]))
279 out = run_fm(prompt, schema, opts["model"], opts["timeout_s"])
294 out = run_fm(prompt, schema, opts["model"], opts["timeout_s"])
@@ -304,7 +319,7 @@ def op_organize(payload, opts):
304 return validate.organize_from_two_calls(labels, assign, payload), len(p1) + len(p2), "two-call"
319 return validate.organize_from_two_calls(labels, assign, payload), len(p1) + len(p2), "two-call"
305 except validate.ValidationError as e:
320 except validate.ValidationError as e:
306 raise Fail("BAD_MODEL_OUTPUT", str(e), retryable=True)
321 raise Fail("BAD_MODEL_OUTPUT", str(e), retryable=True)
307− raise Fail("SCHEMA_MISSING", os.path.join(SCHEMAS, "organize.json"))
322+ raise Fail("SCHEMA_MISSING", f"fm could not write {os.path.join(SCHEMAS, 'organize.json')}")
310def handle(req):
325def handle(req):
@@ -508,7 +523,7 @@ def manifest_problems(path, host_path):
508 problems.append(f"path is {m.get('path')!r}")
523 problems.append(f"path is {m.get('path')!r}")
509 elif not os.access(host_path, os.X_OK):
524 elif not os.access(host_path, os.X_OK):
510 problems.append(f"host {host_path} is missing or not executable")
525 problems.append(f"host {host_path} is missing or not executable")
511− if m.get("allowed_origins") != [ALLOWED_ORIGIN]:
526+ if m.get("allowed_origins") != allowed_origins():
512 problems.append(f"allowed_origins is {m.get('allowed_origins')}")
527 problems.append(f"allowed_origins is {m.get('allowed_origins')}")
513 return problems
528 return problems
@@ -569,11 +584,15 @@ def unregister():
570def manifest_for(path):
585def manifest_for(path):
571 return {"name": HOST_NAME, "description": "Diagonal: names tab groups with Apple's on-device model",
586 return {"name": HOST_NAME, "description": "Diagonal: names tab groups with Apple's on-device model",
572− "path": os.path.abspath(path), "type": "stdio", "allowed_origins": [ALLOWED_ORIGIN]}
587+ "path": os.path.abspath(path), "type": "stdio", "allowed_origins": allowed_origins()}
590+def allowed_origins():
591+ return [o for o in (ALLOWED_ORIGIN, STORE_ORIGIN) if o]
575def origin_ok(arg):
594def origin_ok(arg):
576− return isinstance(arg, str) and arg.rstrip("/") + "/" == ALLOWED_ORIGIN
595+ return isinstance(arg, str) and arg.rstrip("/") + "/" in allowed_origins()
579def main(argv=None):
598def main(argv=None):